diff --git a/docs/admin-adfahi-research/01-dashboard.json b/docs/admin-adfahi-research/01-dashboard.json new file mode 100644 index 0000000..56520b5 --- /dev/null +++ b/docs/admin-adfahi-research/01-dashboard.json @@ -0,0 +1,62 @@ +{ + "label": "数据概览", + "slug": "01-dashboard", + "route": "/dashboard", + "url": "https://admin.adfahi.cn/dashboard", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/01-dashboard.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n总用户数\n16\n活跃用户\n0\n总群组数\n6\n总会话数\n11\n总消息数\n220\n今日消息\n0\n用户增长趋势\n日期\t数量\n2026-08-28\t0\n2026-08-29\t0\n2026-08-30\t0\n2026-08-31\t0\n2026-09-01\t0\n2026-09-02\t0\n2026-09-03\t0\n消息增长趋势\n日期\t数量\n2026-08-28\t0\n2026-08-29\t0\n2026-08-30\t0\n2026-08-31\t0\n2026-09-01\t0\n2026-09-02\t0\n2026-09-03\t0", + "tables": [ + { + "headers": [ + "日期", + "数量" + ], + "idx": 0, + "rows": [ + [ + "2026-08-28", + "0" + ], + [ + "2026-08-29", + "0" + ], + [ + "2026-08-30", + "0" + ] + ] + }, + { + "headers": [ + "日期", + "数量" + ], + "idx": 1, + "rows": [ + [ + "2026-08-28", + "0" + ], + [ + "2026-08-29", + "0" + ], + [ + "2026-08-30", + "0" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/01-dashboard.queried.json b/docs/admin-adfahi-research/01-dashboard.queried.json new file mode 100644 index 0000000..fffd71a --- /dev/null +++ b/docs/admin-adfahi-research/01-dashboard.queried.json @@ -0,0 +1,78 @@ +{ + "label": "数据概览", + "slug": "01-dashboard", + "route": "/dashboard", + "url": "https://admin.adfahi.cn/dashboard", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/01-dashboard.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n总用户数\n16\n活跃用户\n0\n总群组数\n6\n总会话数\n11\n总消息数\n220\n今日消息\n0\n用户增长趋势\n日期\t数量\n2026-08-28\t0\n2026-08-29\t0\n2026-08-30\t0\n2026-08-31\t0\n2026-09-01\t0\n2026-09-02\t0\n2026-09-03\t0\n消息增长趋势\n日期\t数量\n2026-08-28\t0\n2026-08-29\t0\n2026-08-30\t0\n2026-08-31\t0\n2026-09-01\t0\n2026-09-02\t0\n2026-09-03\t0", + "tables": [ + { + "headers": [ + "日期", + "数量" + ], + "idx": 0, + "rows": [ + [ + "2026-08-28", + "0" + ], + [ + "2026-08-29", + "0" + ], + [ + "2026-08-30", + "0" + ], + [ + "2026-08-31", + "0" + ], + [ + "2026-09-01", + "0" + ] + ] + }, + { + "headers": [ + "日期", + "数量" + ], + "idx": 1, + "rows": [ + [ + "2026-08-28", + "0" + ], + [ + "2026-08-29", + "0" + ], + [ + "2026-08-30", + "0" + ], + [ + "2026-08-31", + "0" + ], + [ + "2026-09-01", + "0" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/02-user-management.json b/docs/admin-adfahi-research/02-user-management.json new file mode 100644 index 0000000..3f7f177 --- /dev/null +++ b/docs/admin-adfahi-research/02-user-management.json @@ -0,0 +1,866 @@ +{ + "label": "用户管理", + "slug": "02-user-management", + "route": "/users", + "url": "https://admin.adfahi.cn/users", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/02-user-management.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户管理\n编号\n用户分组\n全部\n状态\n全部\n关键词\n查 询\n重 置\n创建用户\n批量创建\n导入Excel\n下载模板\n编号\t头像\t用户名\t昵称\t手机号\t注册方式\t状态\t用户分组\t最近登录\t创建时间\t操作\n\n编号\n\t\n头像\n\t\n用户名\n\t\n昵称\n\t\n手机号\n\t\n注册方式\n\t\n状态\n\t\n用户分组\n\t\n最近登录\n\t\n创建时间\n\t\n操作\n\n16\t\n点击查看\n\thy73074162\tpoter001\t\t后台创建\t正常\t默认用户组\t2026-08-01 00:05:30\t2026-08-01 00:04:25\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n15\t\n点击查看\n\thy17716319\t往事随风\t\t后台创建\t正常\t默认用户组\t2026-08-01 21:16:15\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n14\t\n点击查看\n\thy34899108\t坚守的山羊\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n13\t\n点击查看\n\thy45257043\t沉稳的松鼠\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n12\t\n点击查看\n\thy69598195\t专注的白兔\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n11\t\n点击查看\n\thy20144467\t沉稳的黑猫\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n10\t\n点击查看\n\thy70299474\t坚韧的河马\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n9\t\n点击查看\n\thy23799134\t求真的猫头\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n8\t\n点击查看\n\thy18959373\t进取的北狐\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n7\t\n点击查看\n\thy80424632\t温润的乌鸦\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n6\t\n点击查看\n\thy98400466\t乐观的野马\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n5\t\n点击查看\n\thy99111080\tF\t13800001115\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:43:53\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n4\t\n点击查看\n\thy92120233\tF\t13800001114\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:43:37\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n3\t\n点击查看\n\thy69089908\tF\t13800001113\t后台创建\t正常\t默认用户组\t2026-07-31 23:26:17\t2026-07-31 22:43:12\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n2\t\n点击查看\n\thy91144536\t冯仁飞\t13800001112\t后台创建\t正常\t默认用户组\t2026-08-02 22:27:59\t2026-07-31 22:43:02\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n1\t\n点击查看\n\thy50703413\t冯宇昂\t13800001111\t后台创建\t正常\t默认用户组\t2026-07-31 23:37:42\t2026-07-31 22:42:45\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n1\n20 条/页", + "tables": [ + { + "headers": [ + "编号", + "头像", + "用户名", + "昵称", + "手机号", + "注册方式", + "状态", + "用户分组", + "最近登录", + "创建时间", + "操作", + "编号", + "头像", + "用户名", + "昵称", + "手机号", + "注册方式", + "状态", + "用户分组", + "最近登录", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "16", + "点击查看", + "hy73074162", + "poter001", + "后台创建", + "正常", + "默认用户组", + "2026-08-01 00:05:30", + "2026-08-01 00:04:25", + "详 情重置密码IP白名单调整分组封 禁强制下线强制注销" + ], + [ + "15", + "点击查看", + "hy17716319", + "往事随风", + "后台创建", + "正常", + "默认用户组", + "2026-08-01 21:16:15", + "2026-07-31 22:44:06", + "详 情重置密码IP白名单调整分组封 禁强制下线强制注销" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "用户编号", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "用户名/昵称/手机号", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "创建用户" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "批量创建" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "导入Excel" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载模板" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/02-user-management.queried.json b/docs/admin-adfahi-research/02-user-management.queried.json new file mode 100644 index 0000000..efdd673 --- /dev/null +++ b/docs/admin-adfahi-research/02-user-management.queried.json @@ -0,0 +1,1022 @@ +{ + "label": "用户管理", + "slug": "02-user-management", + "route": "/users", + "url": "https://admin.adfahi.cn/users", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/02-user-management.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户管理\n编号\n用户分组\n全部\n状态\n全部\n关键词\n查 询\n重 置\n创建用户\n批量创建\n导入Excel\n下载模板\n编号\t头像\t用户名\t昵称\t手机号\t注册方式\t状态\t用户分组\t最近登录\t创建时间\t操作\n\n编号\n\t\n头像\n\t\n用户名\n\t\n昵称\n\t\n手机号\n\t\n注册方式\n\t\n状态\n\t\n用户分组\n\t\n最近登录\n\t\n创建时间\n\t\n操作\n\n16\t\n点击查看\n\thy73074162\tpoter001\t\t后台创建\t正常\t默认用户组\t2026-08-01 00:05:30\t2026-08-01 00:04:25\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n15\t\n点击查看\n\thy17716319\t往事随风\t\t后台创建\t正常\t默认用户组\t2026-08-01 21:16:15\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n14\t\n点击查看\n\thy34899108\t坚守的山羊\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n13\t\n点击查看\n\thy45257043\t沉稳的松鼠\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n12\t\n点击查看\n\thy69598195\t专注的白兔\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n11\t\n点击查看\n\thy20144467\t沉稳的黑猫\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n10\t\n点击查看\n\thy70299474\t坚韧的河马\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n9\t\n点击查看\n\thy23799134\t求真的猫头\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n8\t\n点击查看\n\thy18959373\t进取的北狐\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n7\t\n点击查看\n\thy80424632\t温润的乌鸦\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n6\t\n点击查看\n\thy98400466\t乐观的野马\t\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:44:06\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n5\t\n点击查看\n\thy99111080\tF\t13800001115\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:43:53\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n4\t\n点击查看\n\thy92120233\tF\t13800001114\t后台创建\t正常\t默认用户组\t--\t2026-07-31 22:43:37\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n3\t\n点击查看\n\thy69089908\tF\t13800001113\t后台创建\t正常\t默认用户组\t2026-07-31 23:26:17\t2026-07-31 22:43:12\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n2\t\n点击查看\n\thy91144536\t冯仁飞\t13800001112\t后台创建\t正常\t默认用户组\t2026-08-02 22:27:59\t2026-07-31 22:43:02\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n\n1\t\n点击查看\n\thy50703413\t冯宇昂\t13800001111\t后台创建\t正常\t默认用户组\t2026-07-31 23:37:42\t2026-07-31 22:42:45\t\n详 情\n重置密码\nIP白名单\n调整分组\n封 禁\n强制下线\n强制注销\n1\n20 条/页", + "tables": [ + { + "headers": [ + "编号", + "头像", + "用户名", + "昵称", + "手机号", + "注册方式", + "状态", + "用户分组", + "最近登录", + "创建时间", + "操作", + "编号", + "头像", + "用户名", + "昵称", + "手机号", + "注册方式", + "状态", + "用户分组", + "最近登录", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "16", + "点击查看", + "hy73074162", + "poter001", + "后台创建", + "正常", + "默认用户组", + "2026-08-01 00:05:30", + "2026-08-01 00:04:25", + "详 情重置密码IP白名单调整分组封 禁强制下线强制注销" + ], + [ + "15", + "点击查看", + "hy17716319", + "往事随风", + "后台创建", + "正常", + "默认用户组", + "2026-08-01 21:16:15", + "2026-07-31 22:44:06", + "详 情重置密码IP白名单调整分组封 禁强制下线强制注销" + ], + [ + "14", + "点击查看", + "hy34899108", + "坚守的山羊", + "后台创建", + "正常", + "默认用户组", + "--", + "2026-07-31 22:44:06", + "详 情重置密码IP白名单调整分组封 禁强制下线强制注销" + ], + [ + "13", + "点击查看", + "hy45257043", + "沉稳的松鼠", + "后台创建", + "正常", + "默认用户组", + "--", + "2026-07-31 22:44:06", + "详 情重置密码IP白名单调整分组封 禁强制下线强制注销" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "用户编号", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "用户名/昵称/手机号", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "创建用户", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "批量创建", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "导入Excel", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载模板", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重置密码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "调整分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制下线", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "强制注销", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/03-user-groups.json b/docs/admin-adfahi-research/03-user-groups.json new file mode 100644 index 0000000..0a7e96a --- /dev/null +++ b/docs/admin-adfahi-research/03-user-groups.json @@ -0,0 +1,102 @@ +{ + "label": "用户分组", + "slug": "03-user-groups", + "route": "/user-policy-groups", + "url": "https://admin.adfahi.cn/user-policy-groups", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/03-user-groups.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户分组\n添加分组\nIP 白名单\n租户渠道 IP 白名单\nAPP:未配置\nWeb:未配置\n分组名称\t用户数\t允许加好友\t允许建群\t直接加好友\t好友上限\tAPP 登录\tWeb 登录\tIP 白名单\t状态\t权重\t操作\n\n分组名称\n\t\n用户数\n\t\n允许加好友\n\t\n允许建群\n\t\n直接加好友\n\t\n好友上限\n\t\nAPP 登录\n\t\nWeb 登录\n\t\nIP 白名单\n\t\n状态\n\t\n权重\n\t\n操作\n\n\n默认用户组\n默认\n\t16\t允许\t允许\t--\t不限\t允许\t允许\t未启用\t启用\t0\t\n编辑\n删除", + "tables": [ + { + "headers": [ + "分组名称", + "用户数", + "允许加好友", + "允许建群", + "直接加好友", + "好友上限", + "APP 登录", + "Web 登录", + "IP 白名单", + "状态", + "权重", + "操作", + "分组名称", + "用户数", + "允许加好友", + "允许建群", + "直接加好友", + "好友上限", + "APP 登录", + "Web 登录", + "IP 白名单", + "状态", + "权重", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "默认用户组默认", + "16", + "允许", + "允许", + "--", + "不限", + "允许", + "允许", + "未启用", + "启用", + "0", + "编辑删除" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "添加分组" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP 白名单" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "编辑" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "删除" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/03-user-groups.queried.json b/docs/admin-adfahi-research/03-user-groups.queried.json new file mode 100644 index 0000000..2021c51 --- /dev/null +++ b/docs/admin-adfahi-research/03-user-groups.queried.json @@ -0,0 +1,108 @@ +{ + "label": "用户分组", + "slug": "03-user-groups", + "route": "/user-policy-groups", + "url": "https://admin.adfahi.cn/user-policy-groups", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/03-user-groups.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户分组\n添加分组\nIP 白名单\n租户渠道 IP 白名单\nAPP:未配置\nWeb:未配置\n分组名称\t用户数\t允许加好友\t允许建群\t直接加好友\t好友上限\tAPP 登录\tWeb 登录\tIP 白名单\t状态\t权重\t操作\n\n分组名称\n\t\n用户数\n\t\n允许加好友\n\t\n允许建群\n\t\n直接加好友\n\t\n好友上限\n\t\nAPP 登录\n\t\nWeb 登录\n\t\nIP 白名单\n\t\n状态\n\t\n权重\n\t\n操作\n\n\n默认用户组\n默认\n\t16\t允许\t允许\t--\t不限\t允许\t允许\t未启用\t启用\t0\t\n编辑\n删除", + "tables": [ + { + "headers": [ + "分组名称", + "用户数", + "允许加好友", + "允许建群", + "直接加好友", + "好友上限", + "APP 登录", + "Web 登录", + "IP 白名单", + "状态", + "权重", + "操作", + "分组名称", + "用户数", + "允许加好友", + "允许建群", + "直接加好友", + "好友上限", + "APP 登录", + "Web 登录", + "IP 白名单", + "状态", + "权重", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "默认用户组默认", + "16", + "允许", + "允许", + "--", + "不限", + "允许", + "允许", + "未启用", + "启用", + "0", + "编辑删除" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "添加分组", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "IP 白名单", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "编辑", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "删除", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/04-group-management.json b/docs/admin-adfahi-research/04-group-management.json new file mode 100644 index 0000000..a1b6d44 --- /dev/null +++ b/docs/admin-adfahi-research/04-group-management.json @@ -0,0 +1,296 @@ +{ + "label": "群组管理", + "slug": "04-group-management", + "route": "/groups", + "url": "https://admin.adfahi.cn/groups", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/04-group-management.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n群组管理\n群号\n关键词\n状态\n全部\n查 询\n重 置\n群组编号\t群号\t群组名称\t成员数\t群主昵称\t群主用户名\t状态\t创建时间\t操作\n\n群组编号\n\t\n群号\n\t\n群组名称\n\t\n成员数\n\t\n群主昵称\n\t\n群主用户名\n\t\n状态\n\t\n创建时间\n\t\n操作\n\n6\t68127154\t\n冯\n\t1\tF\thy69089908\t正常\t2026-07-31 22:50:52\t\n详 情\n群文件\n封 禁\n解 散\n\n5\t62262588\t\n冯\n\t3\tF\thy69089908\t正常\t2026-07-31 22:50:38\t\n详 情\n群文件\n封 禁\n解 散\n\n4\t75874312\t\n346亿916\n\t1\t冯宇昂\thy50703413\t正常\t2026-07-31 22:47:57\t\n详 情\n群文件\n封 禁\n解 散\n\n3\t92851806\t\n64919\n\t1\t冯宇昂\thy50703413\t正常\t2026-07-31 22:47:49\t\n详 情\n群文件\n封 禁\n解 散\n\n2\t89233971\t\n冯氏家族\n\t3\t冯仁飞\thy91144536\t正常\t2026-07-31 22:46:59\t\n详 情\n群文件\n封 禁\n解 散\n\n1\t17776111\t\n冯氏家族\n\t3\t冯宇昂\thy50703413\t正常\t2026-07-31 22:45:28\t\n详 情\n群文件\n封 禁\n解 散\n1\n20 条/页", + "tables": [ + { + "headers": [ + "群组编号", + "群号", + "群组名称", + "成员数", + "群主昵称", + "群主用户名", + "状态", + "创建时间", + "操作", + "群组编号", + "群号", + "群组名称", + "成员数", + "群主昵称", + "群主用户名", + "状态", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "6", + "68127154", + "冯", + "1", + "F", + "hy69089908", + "正常", + "2026-07-31 22:50:52", + "详 情群文件封 禁解 散" + ], + [ + "5", + "62262588", + "冯", + "3", + "F", + "hy69089908", + "正常", + "2026-07-31 22:50:38", + "详 情群文件封 禁解 散" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "群号", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "群名称", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/04-group-management.queried.json b/docs/admin-adfahi-research/04-group-management.queried.json new file mode 100644 index 0000000..88cb933 --- /dev/null +++ b/docs/admin-adfahi-research/04-group-management.queried.json @@ -0,0 +1,356 @@ +{ + "label": "群组管理", + "slug": "04-group-management", + "route": "/groups", + "url": "https://admin.adfahi.cn/groups", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/04-group-management.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n群组管理\n群号\n关键词\n状态\n全部\n查 询\n重 置\n群组编号\t群号\t群组名称\t成员数\t群主昵称\t群主用户名\t状态\t创建时间\t操作\n\n群组编号\n\t\n群号\n\t\n群组名称\n\t\n成员数\n\t\n群主昵称\n\t\n群主用户名\n\t\n状态\n\t\n创建时间\n\t\n操作\n\n6\t68127154\t\n冯\n\t1\tF\thy69089908\t正常\t2026-07-31 22:50:52\t\n详 情\n群文件\n封 禁\n解 散\n\n5\t62262588\t\n冯\n\t3\tF\thy69089908\t正常\t2026-07-31 22:50:38\t\n详 情\n群文件\n封 禁\n解 散\n\n4\t75874312\t\n346亿916\n\t1\t冯宇昂\thy50703413\t正常\t2026-07-31 22:47:57\t\n详 情\n群文件\n封 禁\n解 散\n\n3\t92851806\t\n64919\n\t1\t冯宇昂\thy50703413\t正常\t2026-07-31 22:47:49\t\n详 情\n群文件\n封 禁\n解 散\n\n2\t89233971\t\n冯氏家族\n\t3\t冯仁飞\thy91144536\t正常\t2026-07-31 22:46:59\t\n详 情\n群文件\n封 禁\n解 散\n\n1\t17776111\t\n冯氏家族\n\t3\t冯宇昂\thy50703413\t正常\t2026-07-31 22:45:28\t\n详 情\n群文件\n封 禁\n解 散\n1\n20 条/页", + "tables": [ + { + "headers": [ + "群组编号", + "群号", + "群组名称", + "成员数", + "群主昵称", + "群主用户名", + "状态", + "创建时间", + "操作", + "群组编号", + "群号", + "群组名称", + "成员数", + "群主昵称", + "群主用户名", + "状态", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "6", + "68127154", + "冯", + "1", + "F", + "hy69089908", + "正常", + "2026-07-31 22:50:52", + "详 情群文件封 禁解 散" + ], + [ + "5", + "62262588", + "冯", + "3", + "F", + "hy69089908", + "正常", + "2026-07-31 22:50:38", + "详 情群文件封 禁解 散" + ], + [ + "4", + "75874312", + "346亿916", + "1", + "冯宇昂", + "hy50703413", + "正常", + "2026-07-31 22:47:57", + "详 情群文件封 禁解 散" + ], + [ + "3", + "92851806", + "64919", + "1", + "冯宇昂", + "hy50703413", + "正常", + "2026-07-31 22:47:49", + "详 情群文件封 禁解 散" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "群号", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "群名称", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详 情", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "群文件", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "封 禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解 散", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/05-group-members.json b/docs/admin-adfahi-research/05-group-members.json new file mode 100644 index 0000000..e5b22cb --- /dev/null +++ b/docs/admin-adfahi-research/05-group-members.json @@ -0,0 +1,113 @@ +{ + "label": "群成员管理", + "slug": "05-group-members", + "route": "/group-members", + "url": "https://admin.adfahi.cn/group-members", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/05-group-members.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n群成员管理\n群号\n角色\n全部\n关键词\n查 询\n重 置\n刷 新\n编号\t用户名\t昵称\t群昵称\t角色\t禁言状态\t加入时间\t操作\n\n编号\n\t\n用户名\n\t\n昵称\n\t\n群昵称\n\t\n角色\n\t\n禁言状态\n\t\n加入时间\n\t\n操作\n\n\n暂无数据", + "tables": [ + { + "headers": [ + "编号", + "用户名", + "昵称", + "群昵称", + "角色", + "禁言状态", + "加入时间", + "操作", + "编号", + "用户名", + "昵称", + "群昵称", + "角色", + "禁言状态", + "加入时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "暂无数据暂无数据" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "必填", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "用户名/昵称", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "刷 新" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/05-group-members.queried.json b/docs/admin-adfahi-research/05-group-members.queried.json new file mode 100644 index 0000000..d1d9ec3 --- /dev/null +++ b/docs/admin-adfahi-research/05-group-members.queried.json @@ -0,0 +1,173 @@ +{ + "label": "群成员管理", + "slug": "05-group-members", + "route": "/group-members", + "url": "https://admin.adfahi.cn/group-members", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/05-group-members.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n群成员管理\n群号\n角色\n全部\n关键词\n查 询\n重 置\n刷 新\n编号\t用户名\t昵称\t群昵称\t角色\t禁言状态\t加入时间\t操作\n\n编号\n\t\n用户名\n\t\n昵称\n\t\n群昵称\n\t\n角色\n\t\n禁言状态\n\t\n加入时间\n\t\n操作\n\n3\thy69089908\tF\t--\t群主\t正常\t2026-07-31 22:50:52\t\n禁 言\n踢 出\n1\n20 条/页", + "tables": [ + { + "headers": [ + "编号", + "用户名", + "昵称", + "群昵称", + "角色", + "禁言状态", + "加入时间", + "操作", + "编号", + "用户名", + "昵称", + "群昵称", + "角色", + "禁言状态", + "加入时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "3", + "hy69089908", + "F", + "--", + "群主", + "正常", + "2026-07-31 22:50:52", + "禁 言踢 出" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "必填", + "tag": "INPUT", + "text": "", + "value": "68127154" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "用户名/昵称", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "刷 新", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "禁 言", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "踢 出", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/06-invite-codes.json b/docs/admin-adfahi-research/06-invite-codes.json new file mode 100644 index 0000000..697f08c --- /dev/null +++ b/docs/admin-adfahi-research/06-invite-codes.json @@ -0,0 +1,103 @@ +{ + "label": "邀请码管理", + "slug": "06-invite-codes", + "route": "/invite-codes", + "url": "https://admin.adfahi.cn/invite-codes", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/06-invite-codes.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n邀请码管理\n状态\n全部\n邀请码\n查 询\n重 置\n生成邀请码\n每次生成 1 个邀请码,可多次使用。可指定企业与用户;勾选「不限制时间」则永久有效;开启「自动加好友」后,使用该码注册的用户将直接成为指定用户的好友。点击「查看」可查看使用该邀请码注册的用户列表。\n邀请码\t指定用户\t自动加好友\t使用人数\t创建人\t状态\t过期时间\t创建时间\t操作\n\n邀请码\n\t\n指定用户\n\t\n自动加好友\n\t\n使用人数\n\t\n创建人\n\t\n状态\n\t\n过期时间\n\t\n创建时间\n\t\n操作\n\n\n暂无数据", + "tables": [ + { + "headers": [ + "邀请码", + "指定用户", + "自动加好友", + "使用人数", + "创建人", + "状态", + "过期时间", + "创建时间", + "操作", + "邀请码", + "指定用户", + "自动加好友", + "使用人数", + "创建人", + "状态", + "过期时间", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "暂无数据暂无数据" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "6 位数字", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "生成邀请码" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/06-invite-codes.queried.json b/docs/admin-adfahi-research/06-invite-codes.queried.json new file mode 100644 index 0000000..21788ec --- /dev/null +++ b/docs/admin-adfahi-research/06-invite-codes.queried.json @@ -0,0 +1,162 @@ +{ + "label": "邀请码管理", + "slug": "06-invite-codes", + "route": "/invite-codes", + "url": "https://admin.adfahi.cn/invite-codes", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/06-invite-codes.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n用户管理\n用户分组\n群组管理\n群成员管理\n邀请码管理\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n邀请码管理\n状态\n全部\n邀请码\n查 询\n重 置\n生成邀请码\n每次生成 1 个邀请码,可多次使用。可指定企业与用户;勾选「不限制时间」则永久有效;开启「自动加好友」后,使用该码注册的用户将直接成为指定用户的好友。点击「查看」可查看使用该邀请码注册的用户列表。\n邀请码\t指定用户\t自动加好友\t使用人数\t创建人\t状态\t过期时间\t创建时间\t操作\n\n邀请码\n\t\n指定用户\n\t\n自动加好友\n\t\n使用人数\n\t\n创建人\n\t\n状态\n\t\n过期时间\n\t\n创建时间\n\t\n操作\n\n323441\t往事随风 (#2271)\t否\t0\t16\t启用\t2026-09-05 00:34:55\t2026-09-04 00:34:55\t\n查 看\n删 除\n1\n20 条/页", + "tables": [ + { + "headers": [ + "邀请码", + "指定用户", + "自动加好友", + "使用人数", + "创建人", + "状态", + "过期时间", + "创建时间", + "操作", + "邀请码", + "指定用户", + "自动加好友", + "使用人数", + "创建人", + "状态", + "过期时间", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "323441", + "往事随风 (#2271)", + "否", + "0", + "16", + "启用", + "2026-09-05 00:34:55", + "2026-09-04 00:34:55", + "查 看删 除" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "6 位数字", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "生成邀请码", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 看", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/07-direct-messages.json b/docs/admin-adfahi-research/07-direct-messages.json new file mode 100644 index 0000000..a102b6d --- /dev/null +++ b/docs/admin-adfahi-research/07-direct-messages.json @@ -0,0 +1,171 @@ +{ + "label": "单聊消息", + "slug": "07-direct-messages", + "route": "/private-messages", + "url": "https://admin.adfahi.cn/private-messages", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/07-direct-messages.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n单聊消息\n全部\n文本\n图片\n视频\n语音\n文件\n名片\n系统\n发送人用户名\n接收人用户名\n关键词\n开始时间\n结束时间\n查 询\n重 置\n刷 新\n发送人\t接收人\t类型\t内容\t状态\t发送时间\t操作\n\n发送人\n\t\n接收人\n\t\n类型\n\t\n内容\n\t\n状态\n\t\n发送时间\n\t\n操作\n\n\n暂无数据", + "tables": [ + { + "headers": [ + "发送人", + "接收人", + "类型", + "内容", + "状态", + "发送时间", + "操作", + "发送人", + "接收人", + "类型", + "内容", + "状态", + "发送时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "暂无数据暂无数据" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "关键字", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "开始时间", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "结束时间", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "刷 新" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/07-direct-messages.queried.json b/docs/admin-adfahi-research/07-direct-messages.queried.json new file mode 100644 index 0000000..0584cc9 --- /dev/null +++ b/docs/admin-adfahi-research/07-direct-messages.queried.json @@ -0,0 +1,393 @@ +{ + "label": "单聊消息", + "slug": "07-direct-messages", + "route": "/private-messages", + "url": "https://admin.adfahi.cn/private-messages", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/07-direct-messages.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n单聊消息\n全部\n文本\n图片\n视频\n语音\n文件\n名片\n系统\n发送人用户名\n接收人用户名\n关键词\n开始时间\n结束时间\n查 询\n重 置\n刷 新\n发送人\t接收人\t类型\t内容\t状态\t发送时间\t操作\n\n发送人\n\t\n接收人\n\t\n类型\n\t\n内容\n\t\n状态\n\t\n发送时间\n\t\n操作\n\n\nF\nhy69089908\n\t\n冯仁飞\nhy91144536\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:06:05\t\n删 除\n\n\nF\nhy69089908\n\t\n冯宇昂\nhy50703413\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:06:05\t\n删 除\n\n\nF\nhy69089908\n\t\n冯宇昂\nhy50703413\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:05:19\t\n删 除\n\n\nF\nhy69089908\n\t\n冯仁飞\nhy91144536\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:04:12\t\n删 除\n\n\nF\nhy69089908\n\t\n冯宇昂\nhy50703413\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:04:12\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n冯仁飞\nhy91144536\n\t系统\t\n语音通话 00:13\n\t正常\t2026-07-31 23:02:22\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n冯仁飞\nhy91144536\n\t系统\t\n语音通话 00:01\n\t正常\t2026-07-31 23:01:45\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\nF\nhy69089908\n\t系统\t\n语音通话 已取消\n\t正常\t2026-07-31 23:01:36\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\nF\nhy69089908\n\t系统\t\n语音通话 已拒绝\n\t正常\t2026-07-31 23:01:19\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t系统\t\n视频通话 已取消\n\t正常\t2026-07-31 23:00:20\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t系统\t\n视频通话 00:19\n\t正常\t2026-07-31 22:59:59\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t系统\t\n语音通话 00:07\n\t正常\t2026-07-31 22:59:32\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\nF\nhy69089908\n\t系统\t\n语音通话 已取消\n\t正常\t2026-07-31 22:59:05\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t系统\t\n语音通话 00:15\n\t正常\t2026-07-31 22:58:42\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t系统\t\n视频通话 00:07\n\t正常\t2026-07-31 22:58:20\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t系统\t\n视频通话 已取消\n\t正常\t2026-07-31 22:58:07\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\nF\nhy69089908\n\t文本\t\n红心\n\t正常\t2026-07-31 22:51:35\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t文本\t\n1\n\t正常\t2026-07-31 22:51:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\nF\nhy69089908\n\t文本\t\n你想让我\n\t正常\t2026-07-31 22:51:32\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t文本\t\n1\n\t正常\t2026-07-31 22:51:32\t\n删 除\n1\n2\n3\n4\n20 条/页", + "tables": [ + { + "headers": [ + "发送人", + "接收人", + "类型", + "内容", + "状态", + "发送时间", + "操作", + "发送人", + "接收人", + "类型", + "内容", + "状态", + "发送时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "Fhy69089908", + "冯仁飞hy91144536", + "文本", + "哈哈哈哈", + "正常", + "2026-07-31 23:06:05", + "删 除" + ], + [ + "Fhy69089908", + "冯宇昂hy50703413", + "文本", + "哈哈哈哈", + "正常", + "2026-07-31 23:06:05", + "删 除" + ], + [ + "Fhy69089908", + "冯宇昂hy50703413", + "文本", + "哈哈哈哈", + "正常", + "2026-07-31 23:05:19", + "删 除" + ], + [ + "Fhy69089908", + "冯仁飞hy91144536", + "文本", + "哈哈哈哈", + "正常", + "2026-07-31 23:04:12", + "删 除" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "关键字", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "开始时间", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "结束时间", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "刷 新", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/08-group-messages.json b/docs/admin-adfahi-research/08-group-messages.json new file mode 100644 index 0000000..b6ebb47 --- /dev/null +++ b/docs/admin-adfahi-research/08-group-messages.json @@ -0,0 +1,159 @@ +{ + "label": "群聊消息", + "slug": "08-group-messages", + "route": "/group-messages", + "url": "https://admin.adfahi.cn/group-messages", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/08-group-messages.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n群聊消息\n全部\n文本\n图片\n视频\n语音\n文件\n名片\n系统\n发送人用户名\n关键词\n开始时间\n结束时间\n查 询\n重 置\n刷 新\n发送人\t群名称\t类型\t内容\t状态\t发送时间\t操作\n\n发送人\n\t\n群名称\n\t\n类型\n\t\n内容\n\t\n状态\n\t\n发送时间\n\t\n操作\n\n\n暂无数据", + "tables": [ + { + "headers": [ + "发送人", + "群名称", + "类型", + "内容", + "状态", + "发送时间", + "操作", + "发送人", + "群名称", + "类型", + "内容", + "状态", + "发送时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "暂无数据暂无数据" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "关键字", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "开始时间", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "结束时间", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "刷 新" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/08-group-messages.queried.json b/docs/admin-adfahi-research/08-group-messages.queried.json new file mode 100644 index 0000000..81495f8 --- /dev/null +++ b/docs/admin-adfahi-research/08-group-messages.queried.json @@ -0,0 +1,379 @@ +{ + "label": "群聊消息", + "slug": "08-group-messages", + "route": "/group-messages", + "url": "https://admin.adfahi.cn/group-messages", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/08-group-messages.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n群聊消息\n全部\n文本\n图片\n视频\n语音\n文件\n名片\n系统\n发送人用户名\n关键词\n开始时间\n结束时间\n查 询\n重 置\n刷 新\n发送人\t群名称\t类型\t内容\t状态\t发送时间\t操作\n\n发送人\n\t\n群名称\n\t\n类型\n\t\n内容\n\t\n状态\n\t\n发送时间\n\t\n操作\n\n\nF\nhy69089908\n\t\n冯\n68127154\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:06:05\t\n删 除\n\n\nF\nhy69089908\n\t\n冯氏家族\n89233971\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:06:05\t\n删 除\n\n\nF\nhy69089908\n\t\n冯氏家族\n17776111\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:06:05\t\n删 除\n\n\nF\nhy69089908\n\t\n冯\n62262588\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:05:19\t\n删 除\n\n\nF\nhy69089908\n\t\n冯氏家族\n17776111\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:05:19\t\n删 除\n\n\nF\nhy69089908\n\t\n冯氏家族\n89233971\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:05:19\t\n删 除\n\n\nF\nhy69089908\n\t\n冯\n68127154\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:05:19\t\n删 除\n\n\nF\nhy69089908\n\t\n冯\n68127154\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:04:12\t\n删 除\n\n\nF\nhy69089908\n\t\n冯氏家族\n89233971\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:04:12\t\n删 除\n\n\nF\nhy69089908\n\t\n冯氏家族\n17776111\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:04:12\t\n删 除\n\n\nF\nhy69089908\n\t\n冯\n62262588\n\t文本\t\n哈哈哈哈\n\t正常\t2026-07-31 23:03:50\t\n删 除\n\n\nF\nhy69089908\n\t\n冯氏家族\n17776111\n\t文本\t\n😊😊😊😊\n\t正常\t2026-07-31 22:53:42\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\n冯氏家族\n17776111\n\t文本\t\nkddkkffjc\n\t正常\t2026-07-31 22:53:18\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\n冯氏家族\n17776111\n\t文本\t\nkdododdk\n\t正常\t2026-07-31 22:53:17\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\n冯氏家族\n17776111\n\t文本\t\nfkdkkddoso\n\t正常\t2026-07-31 22:53:16\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\n冯氏家族\n17776111\n\t文本\t\nfkkfkfkd\n\t正常\t2026-07-31 22:53:15\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\n冯氏家族\n17776111\n\t文本\t\nfkfkkdod\n\t正常\t2026-07-31 22:53:14\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n冯氏家族\n17776111\n\t文本\t\n敏敏\n\t正常\t2026-07-31 22:53:13\t\n删 除\n\n\n冯仁飞\nhy91144536\n\t\n冯氏家族\n17776111\n\t文本\t\nfkfkfkdo\n\t正常\t2026-07-31 22:53:13\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n冯氏家族\n17776111\n\t文本\t\n您提着\n\t正常\t2026-07-31 22:53:12\t\n删 除\n1\n2\n3\n4\n5\n•••\n8\n20 条/页", + "tables": [ + { + "headers": [ + "发送人", + "群名称", + "类型", + "内容", + "状态", + "发送时间", + "操作", + "发送人", + "群名称", + "类型", + "内容", + "状态", + "发送时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "Fhy69089908", + "冯68127154", + "文本", + "哈哈哈哈", + "正常", + "2026-07-31 23:06:05", + "删 除" + ], + [ + "Fhy69089908", + "冯氏家族89233971", + "文本", + "哈哈哈哈", + "正常", + "2026-07-31 23:06:05", + "删 除" + ], + [ + "Fhy69089908", + "冯氏家族17776111", + "文本", + "哈哈哈哈", + "正常", + "2026-07-31 23:06:05", + "删 除" + ], + [ + "Fhy69089908", + "冯62262588", + "文本", + "哈哈哈哈", + "正常", + "2026-07-31 23:05:19", + "删 除" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "关键字", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "开始时间", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "结束时间", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "刷 新", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/09-sensitive-words.json b/docs/admin-adfahi-research/09-sensitive-words.json new file mode 100644 index 0000000..e3fd372 --- /dev/null +++ b/docs/admin-adfahi-research/09-sensitive-words.json @@ -0,0 +1,135 @@ +{ + "label": "敏感词库", + "slug": "09-sensitive-words", + "route": "/sensitive-words", + "url": "https://admin.adfahi.cn/sensitive-words", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/09-sensitive-words.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n敏感词库\n状态\n全部\n等级\n全部\n关键词\n查 询\n重 置\n新增敏感词\n批量导入\n文件导入\n批量导出\nID\t敏感词\t等级\t替换词\t状态\t创建时间\t操作\n\nID\n\t\n敏感词\n\t\n等级\n\t\n替换词\n\t\n状态\n\t\n创建时间\n\t\n操作\n\n\n暂无数据", + "tables": [ + { + "headers": [ + "ID", + "敏感词", + "等级", + "替换词", + "状态", + "创建时间", + "操作", + "ID", + "敏感词", + "等级", + "替换词", + "状态", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "暂无数据暂无数据" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "敏感词", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "新增敏感词" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "批量导入" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "文件导入" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "批量导出" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/09-sensitive-words.queried.json b/docs/admin-adfahi-research/09-sensitive-words.queried.json new file mode 100644 index 0000000..948f0aa --- /dev/null +++ b/docs/admin-adfahi-research/09-sensitive-words.queried.json @@ -0,0 +1,150 @@ +{ + "label": "敏感词库", + "slug": "09-sensitive-words", + "route": "/sensitive-words", + "url": "https://admin.adfahi.cn/sensitive-words", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/09-sensitive-words.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n敏感词库\n状态\n全部\n等级\n全部\n关键词\n查 询\n重 置\n新增敏感词\n批量导入\n文件导入\n批量导出\nID\t敏感词\t等级\t替换词\t状态\t创建时间\t操作\n\nID\n\t\n敏感词\n\t\n等级\n\t\n替换词\n\t\n状态\n\t\n创建时间\n\t\n操作\n\n\n暂无数据", + "tables": [ + { + "headers": [ + "ID", + "敏感词", + "等级", + "替换词", + "状态", + "创建时间", + "操作", + "ID", + "敏感词", + "等级", + "替换词", + "状态", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "暂无数据暂无数据" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "敏感词", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "新增敏感词", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "批量导入", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "文件导入", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "批量导出", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/10-files.json b/docs/admin-adfahi-research/10-files.json new file mode 100644 index 0000000..ea145d7 --- /dev/null +++ b/docs/admin-adfahi-research/10-files.json @@ -0,0 +1,288 @@ +{ + "label": "文件管理", + "slug": "10-files", + "route": "/file-records", + "url": "https://admin.adfahi.cn/file-records", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/10-files.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n文件管理\n全部\n图片\n视频\n音频\n文档\n其他\n发送人用户名\n文件名\n开始时间\n结束时间\n查 询\n重 置\n刷 新\n发送人\t接收方\t类型\t内容\t文件名\t文件大小\tURL\t状态\t发送时间\t操作\n\n发送人\n\t\n接收方\n\t\n类型\n\t\n内容\n\t\n文件名\n\t\n文件大小\n\t\nURL\n\t\n状态\n\t\n发送时间\n\t\n操作\n\n\n冯宇昂\nhy50703413\n\t\nF\nhy69089908\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:48:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n冯氏家族\n17776111\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:48:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n64919\n92851806\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:48:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n346亿916\n75874312\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:48:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n冯仁飞\nhy91144536\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:46:46\t\n删 除\n1\n20 条/页", + "tables": [ + { + "headers": [ + "发送人", + "接收方", + "类型", + "内容", + "文件名", + "文件大小", + "URL", + "状态", + "发送时间", + "操作", + "发送人", + "接收方", + "类型", + "内容", + "文件名", + "文件大小", + "URL", + "状态", + "发送时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "冯宇昂hy50703413", + "Fhy69089908", + "图片", + "预览下载", + "--", + "--", + "https://gdd-chat-fls.oss-a...", + "正常", + "2026-07-31 22:48:33", + "删 除" + ], + [ + "冯宇昂hy50703413", + "冯氏家族17776111", + "图片", + "预览下载", + "--", + "--", + "https://gdd-chat-fls.oss-a...", + "正常", + "2026-07-31 22:48:33", + "删 除" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "搜索文件名", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "开始时间", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "结束时间", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "刷 新" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/10-files.queried.json b/docs/admin-adfahi-research/10-files.queried.json new file mode 100644 index 0000000..aed0453 --- /dev/null +++ b/docs/admin-adfahi-research/10-files.queried.json @@ -0,0 +1,348 @@ +{ + "label": "文件管理", + "slug": "10-files", + "route": "/file-records", + "url": "https://admin.adfahi.cn/file-records", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/10-files.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n文件管理\n全部\n图片\n视频\n音频\n文档\n其他\n发送人用户名\n文件名\n开始时间\n结束时间\n查 询\n重 置\n刷 新\n发送人\t接收方\t类型\t内容\t文件名\t文件大小\tURL\t状态\t发送时间\t操作\n\n发送人\n\t\n接收方\n\t\n类型\n\t\n内容\n\t\n文件名\n\t\n文件大小\n\t\nURL\n\t\n状态\n\t\n发送时间\n\t\n操作\n\n\n冯宇昂\nhy50703413\n\t\nF\nhy69089908\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:48:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n冯氏家族\n17776111\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:48:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n64919\n92851806\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:48:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n346亿916\n75874312\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:48:33\t\n删 除\n\n\n冯宇昂\nhy50703413\n\t\n冯仁飞\nhy91144536\n\t图片\t\n预览\n下载\n\t--\t--\thttps://gdd-chat-fls.oss-a...\n\t正常\t2026-07-31 22:46:46\t\n删 除\n1\n20 条/页", + "tables": [ + { + "headers": [ + "发送人", + "接收方", + "类型", + "内容", + "文件名", + "文件大小", + "URL", + "状态", + "发送时间", + "操作", + "发送人", + "接收方", + "类型", + "内容", + "文件名", + "文件大小", + "URL", + "状态", + "发送时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "冯宇昂hy50703413", + "Fhy69089908", + "图片", + "预览下载", + "--", + "--", + "https://gdd-chat-fls.oss-a...", + "正常", + "2026-07-31 22:48:33", + "删 除" + ], + [ + "冯宇昂hy50703413", + "冯氏家族17776111", + "图片", + "预览下载", + "--", + "--", + "https://gdd-chat-fls.oss-a...", + "正常", + "2026-07-31 22:48:33", + "删 除" + ], + [ + "冯宇昂hy50703413", + "6491992851806", + "图片", + "预览下载", + "--", + "--", + "https://gdd-chat-fls.oss-a...", + "正常", + "2026-07-31 22:48:33", + "删 除" + ], + [ + "冯宇昂hy50703413", + "346亿91675874312", + "图片", + "预览下载", + "--", + "--", + "https://gdd-chat-fls.oss-a...", + "正常", + "2026-07-31 22:48:33", + "删 除" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "搜索文件名", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "开始时间", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "结束时间", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "刷 新", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "下载", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "删 除", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/11-feedback.json b/docs/admin-adfahi-research/11-feedback.json new file mode 100644 index 0000000..3bda944 --- /dev/null +++ b/docs/admin-adfahi-research/11-feedback.json @@ -0,0 +1,141 @@ +{ + "label": "用户反馈", + "slug": "11-feedback", + "route": "/feedbacks", + "url": "https://admin.adfahi.cn/feedbacks", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/11-feedback.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户反馈\n用户ID\n状态\n全部\n优先级\n全部\n关键词\n查 询\n重 置\n批量处理\n导出 CSV\n\t用户\t优先级\t内容\t状态\t处理人\t创建时间\t操作\n\n\t\n用户\n\t\n优先级\n\t\n内容\n\t\n状态\n\t\n处理人\n\t\n创建时间\n\t\n操作\n\n\n暂无数据", + "tables": [ + { + "headers": [ + "用户", + "优先级", + "内容", + "状态", + "处理人", + "创建时间", + "操作", + "用户", + "优先级", + "内容", + "状态", + "处理人", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "暂无数据暂无数据" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "用户ID", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "内容/联系方式", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "批量处理" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "导出 CSV" + }, + { + "disabled": true, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/11-feedback.queried.json b/docs/admin-adfahi-research/11-feedback.queried.json new file mode 100644 index 0000000..cb3cc50 --- /dev/null +++ b/docs/admin-adfahi-research/11-feedback.queried.json @@ -0,0 +1,157 @@ +{ + "label": "用户反馈", + "slug": "11-feedback", + "route": "/feedbacks", + "url": "https://admin.adfahi.cn/feedbacks", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/11-feedback.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n单聊消息\n群聊消息\n敏感词库\n文件管理\n用户反馈\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户反馈\n用户ID\n状态\n全部\n优先级\n全部\n关键词\n查 询\n重 置\n批量处理\n导出 CSV\n\t用户\t优先级\t内容\t状态\t处理人\t创建时间\t操作\n\n\t\n用户\n\t\n优先级\n\t\n内容\n\t\n状态\n\t\n处理人\n\t\n创建时间\n\t\n操作\n\n\n暂无数据", + "tables": [ + { + "headers": [ + "用户", + "优先级", + "内容", + "状态", + "处理人", + "创建时间", + "操作", + "用户", + "优先级", + "内容", + "状态", + "处理人", + "创建时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "暂无数据暂无数据" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "用户ID", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "内容/联系方式", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查 询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "批量处理", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "导出 CSV", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + }, + { + "disabled": true, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "on" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/12-operation-logs.json b/docs/admin-adfahi-research/12-operation-logs.json new file mode 100644 index 0000000..3058b10 --- /dev/null +++ b/docs/admin-adfahi-research/12-operation-logs.json @@ -0,0 +1,180 @@ +{ + "label": "操作日志", + "slug": "12-operation-logs", + "route": "/audit-logs", + "url": "https://admin.adfahi.cn/audit-logs", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/12-operation-logs.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n操作日志\n动作\n全部\n目标类型\n全部\n目标用户\n目标群\n开始时间\n结束时间\n查询\n重 置\n导出 CSV\n管理员\t动作\t目标类型\t目标\t查询条件\tIP\t时间\n\n管理员\n\t\n动作\n\t\n目标类型\n\t\n目标\n\t\n查询条件\n\t\nIP\n\t\n时间\n\nheiyue\t创建用户\t用户\t\npoter001\nhy73074162\n\t\n--\n\t104.30.173.163\t2026-08-01 00:04:25\nheiyue\t重置密码\t用户\t\n往事随风\nhy17716319\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:23\nheiyue\t重置密码\t用户\t\n坚守的山羊\nhy34899108\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:19\nheiyue\t重置密码\t用户\t\n沉稳的松鼠\nhy45257043\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:16\nheiyue\t重置密码\t用户\t\n专注的白兔\nhy69598195\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:12\nheiyue\t重置密码\t用户\t\n沉稳的黑猫\nhy20144467\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:09\nheiyue\t重置密码\t用户\t\n坚韧的河马\nhy70299474\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:05\nheiyue\t重置密码\t用户\t\n求真的猫头\nhy23799134\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:00\nheiyue\t重置密码\t用户\t\n进取的北狐\nhy18959373\n\t\n--\n\t141.164.107.33\t2026-07-31 22:44:55\nheiyue\t重置密码\t用户\t\n温润的乌鸦\nhy80424632\n\t\n--\n\t141.164.107.33\t2026-07-31 22:44:51\nheiyue\t重置密码\t用户\t\n乐观的野马\nhy98400466\n\t\n--\n\t141.164.107.33\t2026-07-31 22:44:48\nheiyue\t批量创建用户\t用户\t--\t\n详情\n\t141.164.107.33\t2026-07-31 22:44:06\nheiyue\t创建用户\t用户\t\nF\nhy99111080\n\t\n--\n\t141.164.107.33\t2026-07-31 22:43:53\nheiyue\t创建用户\t用户\t\nF\nhy92120233\n\t\n--\n\t141.164.107.33\t2026-07-31 22:43:37\nheiyue\t创建用户\t用户\t\nF\nhy69089908\n\t\n--\n\t141.164.107.33\t2026-07-31 22:43:12\nheiyue\t创建用户\t用户\t\n冯仁飞\nhy91144536\n\t\n--\n\t141.164.107.33\t2026-07-31 22:43:02\nheiyue\t创建用户\t用户\t\n冯宇昂\nhy50703413\n\t\n--\n\t141.164.107.33\t2026-07-31 22:42:45\n1\n20 条/页", + "tables": [ + { + "headers": [ + "管理员", + "动作", + "目标类型", + "目标", + "查询条件", + "IP", + "时间", + "管理员", + "动作", + "目标类型", + "目标", + "查询条件", + "IP", + "时间" + ], + "idx": 0, + "rows": [ + [], + [ + "heiyue", + "创建用户", + "用户", + "poter001hy73074162", + "--", + "104.30.173.163", + "2026-08-01 00:04:25" + ], + [ + "heiyue", + "重置密码", + "用户", + "往事随风hy17716319", + "--", + "141.164.107.33", + "2026-07-31 22:45:23" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "用户名", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "群号", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "导出 CSV" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详情" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/12-operation-logs.queried.json b/docs/admin-adfahi-research/12-operation-logs.queried.json new file mode 100644 index 0000000..65388d3 --- /dev/null +++ b/docs/admin-adfahi-research/12-operation-logs.queried.json @@ -0,0 +1,218 @@ +{ + "label": "操作日志", + "slug": "12-operation-logs", + "route": "/audit-logs", + "url": "https://admin.adfahi.cn/audit-logs", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/12-operation-logs.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n操作日志\n动作\n全部\n目标类型\n全部\n目标用户\n目标群\n开始时间\n结束时间\n查询\n重 置\n导出 CSV\n管理员\t动作\t目标类型\t目标\t查询条件\tIP\t时间\n\n管理员\n\t\n动作\n\t\n目标类型\n\t\n目标\n\t\n查询条件\n\t\nIP\n\t\n时间\n\nheiyue\t创建用户\t用户\t\npoter001\nhy73074162\n\t\n--\n\t104.30.173.163\t2026-08-01 00:04:25\nheiyue\t重置密码\t用户\t\n往事随风\nhy17716319\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:23\nheiyue\t重置密码\t用户\t\n坚守的山羊\nhy34899108\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:19\nheiyue\t重置密码\t用户\t\n沉稳的松鼠\nhy45257043\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:16\nheiyue\t重置密码\t用户\t\n专注的白兔\nhy69598195\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:12\nheiyue\t重置密码\t用户\t\n沉稳的黑猫\nhy20144467\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:09\nheiyue\t重置密码\t用户\t\n坚韧的河马\nhy70299474\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:05\nheiyue\t重置密码\t用户\t\n求真的猫头\nhy23799134\n\t\n--\n\t141.164.107.33\t2026-07-31 22:45:00\nheiyue\t重置密码\t用户\t\n进取的北狐\nhy18959373\n\t\n--\n\t141.164.107.33\t2026-07-31 22:44:55\nheiyue\t重置密码\t用户\t\n温润的乌鸦\nhy80424632\n\t\n--\n\t141.164.107.33\t2026-07-31 22:44:51\nheiyue\t重置密码\t用户\t\n乐观的野马\nhy98400466\n\t\n--\n\t141.164.107.33\t2026-07-31 22:44:48\nheiyue\t批量创建用户\t用户\t--\t\n详情\n\t141.164.107.33\t2026-07-31 22:44:06\nheiyue\t创建用户\t用户\t\nF\nhy99111080\n\t\n--\n\t141.164.107.33\t2026-07-31 22:43:53\nheiyue\t创建用户\t用户\t\nF\nhy92120233\n\t\n--\n\t141.164.107.33\t2026-07-31 22:43:37\nheiyue\t创建用户\t用户\t\nF\nhy69089908\n\t\n--\n\t141.164.107.33\t2026-07-31 22:43:12\nheiyue\t创建用户\t用户\t\n冯仁飞\nhy91144536\n\t\n--\n\t141.164.107.33\t2026-07-31 22:43:02\nheiyue\t创建用户\t用户\t\n冯宇昂\nhy50703413\n\t\n--\n\t141.164.107.33\t2026-07-31 22:42:45\n1\n20 条/页", + "tables": [ + { + "headers": [ + "管理员", + "动作", + "目标类型", + "目标", + "查询条件", + "IP", + "时间", + "管理员", + "动作", + "目标类型", + "目标", + "查询条件", + "IP", + "时间" + ], + "idx": 0, + "rows": [ + [], + [ + "heiyue", + "创建用户", + "用户", + "poter001hy73074162", + "--", + "104.30.173.163", + "2026-08-01 00:04:25" + ], + [ + "heiyue", + "重置密码", + "用户", + "往事随风hy17716319", + "--", + "141.164.107.33", + "2026-07-31 22:45:23" + ], + [ + "heiyue", + "重置密码", + "用户", + "坚守的山羊hy34899108", + "--", + "141.164.107.33", + "2026-07-31 22:45:19" + ], + [ + "heiyue", + "重置密码", + "用户", + "沉稳的松鼠hy45257043", + "--", + "141.164.107.33", + "2026-07-31 22:45:16" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "用户名", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "群号", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "导出 CSV", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "详情", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/13-user-account-audit.json b/docs/admin-adfahi-research/13-user-account-audit.json new file mode 100644 index 0000000..a6d7fec --- /dev/null +++ b/docs/admin-adfahi-research/13-user-account-audit.json @@ -0,0 +1,174 @@ +{ + "label": "用户账号审计", + "slug": "13-user-account-audit", + "route": "/user-account-audit", + "url": "https://admin.adfahi.cn/user-account-audit", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/13-user-account-audit.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户账号审计\n用户编号\n用户名\n操作者\n操作\nIP\n时间\n查询\n重 置\n操作时间\t目标用户\t操作者\t操作类型\t变更摘要\tIP / 归属地\t设备/客户端\n\n操作时间\n\t\n目标用户\n\t\n操作者\n\t\n操作类型\n\t\n变更摘要\n\t\nIP / 归属地\n\t\n设备/客户端\n\n2026-08-01 00:04:25\t\nhy73074162\n编号:16\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n104.30.173.163\n美国 加利福尼亚州 罗斯维尔\n\tMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\n2026-07-31 22:45:23\t\nhy17716319\n编号:15\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:19\t\nhy34899108\n编号:14\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:16\t\nhy45257043\n编号:13\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:12\t\nhy69598195\n编号:12\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:09\t\nhy20144467\n编号:11\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:05\t\nhy70299474\n编号:10\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:00\t\nhy23799134\n编号:9\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:44:55\t\nhy18959373\n编号:8\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:44:51\t\nhy80424632\n编号:7\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:44:48\t\nhy98400466\n编号:6\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:43:53\t\nhy99111080\n编号:5\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:43:37\t\nhy92120233\n编号:4\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:43:12\t\nhy69089908\n编号:3\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:43:02\t\nhy91144536\n编号:2\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:42:45\t\nhy50703413\n编号:1\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n共 16 条\n1\n20 条/页", + "tables": [ + { + "headers": [ + "操作时间", + "目标用户", + "操作者", + "操作类型", + "变更摘要", + "IP / 归属地", + "设备/客户端", + "操作时间", + "目标用户", + "操作者", + "操作类型", + "变更摘要", + "IP / 归属地", + "设备/客户端" + ], + "idx": 0, + "rows": [ + [], + [ + "2026-08-01 00:04:25", + "hy73074162编号:16", + "管理员heiyue", + "create_user", + "create_user", + "104.30.173.163美国 加利福尼亚州 罗斯维尔", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36" + ], + [ + "2026-07-31 22:45:23", + "hy17716319编号:15", + "管理员heiyue", + "管理员重置密码", + "管理员重置密码", + "141.164.107.33老挝 万象市 永珍", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "开始日期", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "结束日期", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/13-user-account-audit.queried.json b/docs/admin-adfahi-research/13-user-account-audit.queried.json new file mode 100644 index 0000000..abfc9ba --- /dev/null +++ b/docs/admin-adfahi-research/13-user-account-audit.queried.json @@ -0,0 +1,211 @@ +{ + "label": "用户账号审计", + "slug": "13-user-account-audit", + "route": "/user-account-audit", + "url": "https://admin.adfahi.cn/user-account-audit", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/13-user-account-audit.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户账号审计\n用户编号\n用户名\n操作者\n操作\nIP\n时间\n查询\n重 置\n操作时间\t目标用户\t操作者\t操作类型\t变更摘要\tIP / 归属地\t设备/客户端\n\n操作时间\n\t\n目标用户\n\t\n操作者\n\t\n操作类型\n\t\n变更摘要\n\t\nIP / 归属地\n\t\n设备/客户端\n\n2026-08-01 00:04:25\t\nhy73074162\n编号:16\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n104.30.173.163\n美国 加利福尼亚州 罗斯维尔\n\tMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\n2026-07-31 22:45:23\t\nhy17716319\n编号:15\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:19\t\nhy34899108\n编号:14\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:16\t\nhy45257043\n编号:13\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:12\t\nhy69598195\n编号:12\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:09\t\nhy20144467\n编号:11\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:05\t\nhy70299474\n编号:10\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:45:00\t\nhy23799134\n编号:9\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:44:55\t\nhy18959373\n编号:8\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:44:51\t\nhy80424632\n编号:7\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:44:48\t\nhy98400466\n编号:6\n\t\n管理员\nheiyue\n\t管理员重置密码\t管理员重置密码\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:43:53\t\nhy99111080\n编号:5\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:43:37\t\nhy92120233\n编号:4\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:43:12\t\nhy69089908\n编号:3\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:43:02\t\nhy91144536\n编号:2\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n2026-07-31 22:42:45\t\nhy50703413\n编号:1\n\t\n管理员\nheiyue\n\tcreate_user\tcreate_user\t\n141.164.107.33\n老挝 万象市 永珍\n\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\n共 16 条\n1\n20 条/页", + "tables": [ + { + "headers": [ + "操作时间", + "目标用户", + "操作者", + "操作类型", + "变更摘要", + "IP / 归属地", + "设备/客户端", + "操作时间", + "目标用户", + "操作者", + "操作类型", + "变更摘要", + "IP / 归属地", + "设备/客户端" + ], + "idx": 0, + "rows": [ + [], + [ + "2026-08-01 00:04:25", + "hy73074162编号:16", + "管理员heiyue", + "create_user", + "create_user", + "104.30.173.163美国 加利福尼亚州 罗斯维尔", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36" + ], + [ + "2026-07-31 22:45:23", + "hy17716319编号:15", + "管理员heiyue", + "管理员重置密码", + "管理员重置密码", + "141.164.107.33老挝 万象市 永珍", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0" + ], + [ + "2026-07-31 22:45:19", + "hy34899108编号:14", + "管理员heiyue", + "管理员重置密码", + "管理员重置密码", + "141.164.107.33老挝 万象市 永珍", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0" + ], + [ + "2026-07-31 22:45:16", + "hy45257043编号:13", + "管理员heiyue", + "管理员重置密码", + "管理员重置密码", + "141.164.107.33老挝 万象市 永珍", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "开始日期", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "结束日期", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/14-group-event-audit.json b/docs/admin-adfahi-research/14-group-event-audit.json new file mode 100644 index 0000000..f93f17b --- /dev/null +++ b/docs/admin-adfahi-research/14-group-event-audit.json @@ -0,0 +1,172 @@ +{ + "label": "群事件审计", + "slug": "14-group-event-audit", + "route": "/group-events", + "url": "https://admin.adfahi.cn/group-events", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/14-group-event-audit.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n群事件审计\n群号\n操作人\n目标用户\n事件类型\n全部\n开始时间\n结束时间\n查询\n重 置\n群名称\t事件类型\t操作人\t目标用户\t可见范围\t旧值\t新值\t时间\n\n群名称\n\t\n事件类型\n\t\n操作人\n\t\n目标用户\n\t\n可见范围\n\t\n旧值\n\t\n新值\n\t\n时间\n\n\n冯\n62262588\n\t成员加入\t\nF\nhy69089908\n\t\n冯宇昂\nhy50703413\n\t全员可见\t--\t--\t2026-07-31 23:03:42\n\n冯\n62262588\n\t成员加入\t\nF\nhy69089908\n\t\n冯仁飞\nhy91144536\n\t全员可见\t--\t--\t2026-07-31 23:03:42\n\n冯\n68127154\n\t群创建\t\nF\nhy69089908\n\t--\t全员可见\t--\t--\t2026-07-31 22:50:52\n\n冯氏家族\n89233971\n\t成员加入\t\n冯仁飞\nhy91144536\n\t\n冯宇昂\nhy50703413\n\t全员可见\t--\t--\t2026-07-31 22:50:43\n\n冯氏家族\n89233971\n\t成员加入\t\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t全员可见\t--\t--\t2026-07-31 22:50:43\n\n冯\n62262588\n\t群创建\t\nF\nhy69089908\n\t--\t全员可见\t--\t--\t2026-07-31 22:50:38\n\n冯氏家族\n17776111\n\t成员加入\t\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t全员可见\t--\t--\t2026-07-31 22:50:34\n\n346亿916\n75874312\n\t群创建\t\n冯宇昂\nhy50703413\n\t--\t全员可见\t--\t--\t2026-07-31 22:47:57\n\n64919\n92851806\n\t群创建\t\n冯宇昂\nhy50703413\n\t--\t全员可见\t--\t--\t2026-07-31 22:47:50\n\n冯氏家族\n17776111\n\t成员加入\t\n冯宇昂\nhy50703413\n\t\n冯仁飞\nhy91144536\n\t全员可见\t--\t--\t2026-07-31 22:47:19\n\n冯氏家族\n89233971\n\t群创建\t\n冯仁飞\nhy91144536\n\t--\t全员可见\t--\t--\t2026-07-31 22:46:59\n\n冯氏家族\n17776111\n\t群创建\t\n冯宇昂\nhy50703413\n\t--\t全员可见\t--\t--\t2026-07-31 22:45:28\n1\n20 条/页", + "tables": [ + { + "headers": [ + "群名称", + "事件类型", + "操作人", + "目标用户", + "可见范围", + "旧值", + "新值", + "时间", + "群名称", + "事件类型", + "操作人", + "目标用户", + "可见范围", + "旧值", + "新值", + "时间" + ], + "idx": 0, + "rows": [ + [], + [ + "冯62262588", + "成员加入", + "Fhy69089908", + "冯宇昂hy50703413", + "全员可见", + "--", + "--", + "2026-07-31 23:03:42" + ], + [ + "冯62262588", + "成员加入", + "Fhy69089908", + "冯仁飞hy91144536", + "全员可见", + "--", + "--", + "2026-07-31 23:03:42" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "用户名", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "用户名", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/14-group-event-audit.queried.json b/docs/admin-adfahi-research/14-group-event-audit.queried.json new file mode 100644 index 0000000..84f4081 --- /dev/null +++ b/docs/admin-adfahi-research/14-group-event-audit.queried.json @@ -0,0 +1,210 @@ +{ + "label": "群事件审计", + "slug": "14-group-event-audit", + "route": "/group-events", + "url": "https://admin.adfahi.cn/group-events", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/14-group-event-audit.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n群事件审计\n群号\n操作人\n目标用户\n事件类型\n全部\n开始时间\n结束时间\n查询\n重 置\n群名称\t事件类型\t操作人\t目标用户\t可见范围\t旧值\t新值\t时间\n\n群名称\n\t\n事件类型\n\t\n操作人\n\t\n目标用户\n\t\n可见范围\n\t\n旧值\n\t\n新值\n\t\n时间\n\n\n冯\n62262588\n\t成员加入\t\nF\nhy69089908\n\t\n冯宇昂\nhy50703413\n\t全员可见\t--\t--\t2026-07-31 23:03:42\n\n冯\n62262588\n\t成员加入\t\nF\nhy69089908\n\t\n冯仁飞\nhy91144536\n\t全员可见\t--\t--\t2026-07-31 23:03:42\n\n冯\n68127154\n\t群创建\t\nF\nhy69089908\n\t--\t全员可见\t--\t--\t2026-07-31 22:50:52\n\n冯氏家族\n89233971\n\t成员加入\t\n冯仁飞\nhy91144536\n\t\n冯宇昂\nhy50703413\n\t全员可见\t--\t--\t2026-07-31 22:50:43\n\n冯氏家族\n89233971\n\t成员加入\t\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t全员可见\t--\t--\t2026-07-31 22:50:43\n\n冯\n62262588\n\t群创建\t\nF\nhy69089908\n\t--\t全员可见\t--\t--\t2026-07-31 22:50:38\n\n冯氏家族\n17776111\n\t成员加入\t\n冯仁飞\nhy91144536\n\t\nF\nhy69089908\n\t全员可见\t--\t--\t2026-07-31 22:50:34\n\n346亿916\n75874312\n\t群创建\t\n冯宇昂\nhy50703413\n\t--\t全员可见\t--\t--\t2026-07-31 22:47:57\n\n64919\n92851806\n\t群创建\t\n冯宇昂\nhy50703413\n\t--\t全员可见\t--\t--\t2026-07-31 22:47:50\n\n冯氏家族\n17776111\n\t成员加入\t\n冯宇昂\nhy50703413\n\t\n冯仁飞\nhy91144536\n\t全员可见\t--\t--\t2026-07-31 22:47:19\n\n冯氏家族\n89233971\n\t群创建\t\n冯仁飞\nhy91144536\n\t--\t全员可见\t--\t--\t2026-07-31 22:46:59\n\n冯氏家族\n17776111\n\t群创建\t\n冯宇昂\nhy50703413\n\t--\t全员可见\t--\t--\t2026-07-31 22:45:28\n1\n20 条/页", + "tables": [ + { + "headers": [ + "群名称", + "事件类型", + "操作人", + "目标用户", + "可见范围", + "旧值", + "新值", + "时间", + "群名称", + "事件类型", + "操作人", + "目标用户", + "可见范围", + "旧值", + "新值", + "时间" + ], + "idx": 0, + "rows": [ + [], + [ + "冯62262588", + "成员加入", + "Fhy69089908", + "冯宇昂hy50703413", + "全员可见", + "--", + "--", + "2026-07-31 23:03:42" + ], + [ + "冯62262588", + "成员加入", + "Fhy69089908", + "冯仁飞hy91144536", + "全员可见", + "--", + "--", + "2026-07-31 23:03:42" + ], + [ + "冯68127154", + "群创建", + "Fhy69089908", + "--", + "全员可见", + "--", + "--", + "2026-07-31 22:50:52" + ], + [ + "冯氏家族89233971", + "成员加入", + "冯仁飞hy91144536", + "冯宇昂hy50703413", + "全员可见", + "--", + "--", + "2026-07-31 22:50:43" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "用户名", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "用户名", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/15-admin-login.json b/docs/admin-adfahi-research/15-admin-login.json new file mode 100644 index 0000000..31b39ae --- /dev/null +++ b/docs/admin-adfahi-research/15-admin-login.json @@ -0,0 +1,102 @@ +{ + "label": "管理员登录", + "slug": "15-admin-login", + "route": "/admin-login-history", + "url": "https://admin.adfahi.cn/admin-login-history", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/15-admin-login.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n管理员登录历史\n开始时间\n结束时间\n查询\n用户名\t登录时间\tUser-Agent\tIP 地址\n\n用户名\n\t\n登录时间\n\t\nUser-Agent\n\t\nIP 地址\n\nheiyue\t2026-09-04 00:10:57\tMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36\t103.136.251.99\nheiyue\t2026-08-01 00:03:52\tMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\t104.30.173.163\nheiyue\t2026-07-31 22:47:31\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\t141.164.107.235\nheiyue\t2026-07-31 22:42:09\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\t141.164.107.33\nheiyue\t2026-07-31 21:32:28\tMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\t104.30.173.163\nheiyue\t2026-07-31 00:45:59\tMozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1\t203.160.80.152\n1\n20 条/页", + "tables": [ + { + "headers": [ + "用户名", + "登录时间", + "User-Agent", + "IP 地址", + "用户名", + "登录时间", + "User-Agent", + "IP 地址" + ], + "idx": 0, + "rows": [ + [], + [ + "heiyue", + "2026-09-04 00:10:57", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + "103.136.251.99" + ], + [ + "heiyue", + "2026-08-01 00:03:52", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", + "104.30.173.163" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/15-admin-login.queried.json b/docs/admin-adfahi-research/15-admin-login.queried.json new file mode 100644 index 0000000..98278ed --- /dev/null +++ b/docs/admin-adfahi-research/15-admin-login.queried.json @@ -0,0 +1,123 @@ +{ + "label": "管理员登录", + "slug": "15-admin-login", + "route": "/admin-login-history", + "url": "https://admin.adfahi.cn/admin-login-history", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/15-admin-login.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n管理员登录历史\n开始时间\n结束时间\n查询\n用户名\t登录时间\tUser-Agent\tIP 地址\n\n用户名\n\t\n登录时间\n\t\nUser-Agent\n\t\nIP 地址\n\nheiyue\t2026-09-04 00:10:57\tMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36\t103.136.251.99\nheiyue\t2026-08-01 00:03:52\tMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\t104.30.173.163\nheiyue\t2026-07-31 22:47:31\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\t141.164.107.235\nheiyue\t2026-07-31 22:42:09\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\t141.164.107.33\nheiyue\t2026-07-31 21:32:28\tMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\t104.30.173.163\nheiyue\t2026-07-31 00:45:59\tMozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1\t203.160.80.152\n1\n20 条/页", + "tables": [ + { + "headers": [ + "用户名", + "登录时间", + "User-Agent", + "IP 地址", + "用户名", + "登录时间", + "User-Agent", + "IP 地址" + ], + "idx": 0, + "rows": [ + [], + [ + "heiyue", + "2026-09-04 00:10:57", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + "103.136.251.99" + ], + [ + "heiyue", + "2026-08-01 00:03:52", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", + "104.30.173.163" + ], + [ + "heiyue", + "2026-07-31 22:47:31", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", + "141.164.107.235" + ], + [ + "heiyue", + "2026-07-31 22:42:09", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0", + "141.164.107.33" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/16-user-login.json b/docs/admin-adfahi-research/16-user-login.json new file mode 100644 index 0000000..ff4f4e7 --- /dev/null +++ b/docs/admin-adfahi-research/16-user-login.json @@ -0,0 +1,202 @@ +{ + "label": "用户登录", + "slug": "16-user-login", + "route": "/user-login-history", + "url": "https://admin.adfahi.cn/user-login-history", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/16-user-login.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户登录历史\n用户编号\n用户名\n开始时间\n结束时间\nIP\n查询\n重 置\n用户编号\t用户名\t当前状态\t登录时间\t设备类型\t设备型号\t系统版本\tApp 版本\tIP 地址\t归属地\t操作\n\n用户编号\n\t\n用户名\n\t\n当前状态\n\t\n登录时间\n\t\n设备类型\n\t\n设备型号\n\t\n系统版本\n\t\nApp 版本\n\t\nIP 地址\n\t\n归属地\n\t\n操作\n\n16\thy73074162\t正常\t2026-08-01 00:04:47\tweb\tChrome Browser\tMac OS X 10.15.7\t-\t104.30.173.163\t美国 加利福尼亚州 罗斯维尔\t\n快速封禁\n\n15\thy17716319\t正常\t2026-07-31 22:50:19\tandroid\tOPPO PCHM10\tAndroid 11\t1.1.2\t141.164.107.33\t老挝 万象市 永珍\t\n快速封禁\n\n3\thy69089908\t正常\t2026-07-31 22:47:10\tandroid\tOPPO PKW110\tAndroid 16\t1.1.2\t61.153.184.95\t浙江 绍兴\t\n快速封禁\n\n2\thy91144536\t正常\t2026-07-31 22:45:28\tandroid\tHONOR LOG-AN00\tAndroid 15\t1.1.2\t61.153.184.95\t浙江 绍兴\t\n快速封禁\n\n1\thy50703413\t正常\t2026-07-31 22:45:10\tandroid\tvivo V1809A\tAndroid 9\t1.1.2\t124.160.160.95\t浙江 杭州\t\n快速封禁\n\n1\thy50703413\t正常\t2026-07-31 22:44:44\tandroid\tHONOR LOG-AN00\tAndroid 15\t1.1.2\t61.153.184.95\t浙江 绍兴\t\n快速封禁\n\n1\thy50703413\t正常\t2026-07-31 22:43:38\tandroid\tvivo V1809A\tAndroid 9\t1.1.2\t124.160.160.95\t浙江 杭州\t\n快速封禁\n1\n20 条/页", + "tables": [ + { + "headers": [ + "用户编号", + "用户名", + "当前状态", + "登录时间", + "设备类型", + "设备型号", + "系统版本", + "App 版本", + "IP 地址", + "归属地", + "操作", + "用户编号", + "用户名", + "当前状态", + "登录时间", + "设备类型", + "设备型号", + "系统版本", + "App 版本", + "IP 地址", + "归属地", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "16", + "hy73074162", + "正常", + "2026-08-01 00:04:47", + "web", + "Chrome Browser", + "Mac OS X 10.15.7", + "-", + "104.30.173.163", + "美国 加利福尼亚州 罗斯维尔", + "快速封禁" + ], + [ + "15", + "hy17716319", + "正常", + "2026-07-31 22:50:19", + "android", + "OPPO PCHM10", + "Android 11", + "1.1.2", + "141.164.107.33", + "老挝 万象市 永珍", + "快速封禁" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "登录IP", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/16-user-login.queried.json b/docs/admin-adfahi-research/16-user-login.queried.json new file mode 100644 index 0000000..6b5eba4 --- /dev/null +++ b/docs/admin-adfahi-research/16-user-login.queried.json @@ -0,0 +1,249 @@ +{ + "label": "用户登录", + "slug": "16-user-login", + "route": "/user-login-history", + "url": "https://admin.adfahi.cn/user-login-history", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/16-user-login.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n用户登录历史\n用户编号\n用户名\n开始时间\n结束时间\nIP\n查询\n重 置\n用户编号\t用户名\t当前状态\t登录时间\t设备类型\t设备型号\t系统版本\tApp 版本\tIP 地址\t归属地\t操作\n\n用户编号\n\t\n用户名\n\t\n当前状态\n\t\n登录时间\n\t\n设备类型\n\t\n设备型号\n\t\n系统版本\n\t\nApp 版本\n\t\nIP 地址\n\t\n归属地\n\t\n操作\n\n16\thy73074162\t正常\t2026-08-01 00:04:47\tweb\tChrome Browser\tMac OS X 10.15.7\t-\t104.30.173.163\t美国 加利福尼亚州 罗斯维尔\t\n快速封禁\n\n15\thy17716319\t正常\t2026-07-31 22:50:19\tandroid\tOPPO PCHM10\tAndroid 11\t1.1.2\t141.164.107.33\t老挝 万象市 永珍\t\n快速封禁\n\n3\thy69089908\t正常\t2026-07-31 22:47:10\tandroid\tOPPO PKW110\tAndroid 16\t1.1.2\t61.153.184.95\t浙江 绍兴\t\n快速封禁\n\n2\thy91144536\t正常\t2026-07-31 22:45:28\tandroid\tHONOR LOG-AN00\tAndroid 15\t1.1.2\t61.153.184.95\t浙江 绍兴\t\n快速封禁\n\n1\thy50703413\t正常\t2026-07-31 22:45:10\tandroid\tvivo V1809A\tAndroid 9\t1.1.2\t124.160.160.95\t浙江 杭州\t\n快速封禁\n\n1\thy50703413\t正常\t2026-07-31 22:44:44\tandroid\tHONOR LOG-AN00\tAndroid 15\t1.1.2\t61.153.184.95\t浙江 绍兴\t\n快速封禁\n\n1\thy50703413\t正常\t2026-07-31 22:43:38\tandroid\tvivo V1809A\tAndroid 9\t1.1.2\t124.160.160.95\t浙江 杭州\t\n快速封禁\n1\n20 条/页", + "tables": [ + { + "headers": [ + "用户编号", + "用户名", + "当前状态", + "登录时间", + "设备类型", + "设备型号", + "系统版本", + "App 版本", + "IP 地址", + "归属地", + "操作", + "用户编号", + "用户名", + "当前状态", + "登录时间", + "设备类型", + "设备型号", + "系统版本", + "App 版本", + "IP 地址", + "归属地", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "16", + "hy73074162", + "正常", + "2026-08-01 00:04:47", + "web", + "Chrome Browser", + "Mac OS X 10.15.7", + "-", + "104.30.173.163", + "美国 加利福尼亚州 罗斯维尔", + "快速封禁" + ], + [ + "15", + "hy17716319", + "正常", + "2026-07-31 22:50:19", + "android", + "OPPO PCHM10", + "Android 11", + "1.1.2", + "141.164.107.33", + "老挝 万象市 永珍", + "快速封禁" + ], + [ + "3", + "hy69089908", + "正常", + "2026-07-31 22:47:10", + "android", + "OPPO PKW110", + "Android 16", + "1.1.2", + "61.153.184.95", + "浙江 绍兴", + "快速封禁" + ], + [ + "2", + "hy91144536", + "正常", + "2026-07-31 22:45:28", + "android", + "HONOR LOG-AN00", + "Android 15", + "1.1.2", + "61.153.184.95", + "浙江 绍兴", + "快速封禁" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "登录IP", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "快速封禁", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/17-ip-account-stats.json b/docs/admin-adfahi-research/17-ip-account-stats.json new file mode 100644 index 0000000..bb0846d --- /dev/null +++ b/docs/admin-adfahi-research/17-ip-account-stats.json @@ -0,0 +1,123 @@ +{ + "label": "IP 账号统计", + "slug": "17-ip-account-stats", + "route": "/ip-user-stats", + "url": "https://admin.adfahi.cn/ip-user-stats", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/17-ip-account-stats.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\nIP 账号统计\nIP\n查询\n重 置\nIP 地址\t归属地\t关联用户\t注册用户\t登录用户\t正常\t已封禁\t最近活动\t操作\n\nIP 地址\n\t\n归属地\n\t\n关联用户\n\t\n注册用户\n\t\n登录用户\n\t\n正常\n\t\n已封禁\n\t\n最近活动\n\t\n操作\n\n61.153.184.95\t浙江 绍兴\t\n3\n多账号\n\t0\t3\t3\t0\t2026-08-02 22:27:59\t\n用户列表\n共 1 个 IP\n1\n20 条/页", + "tables": [ + { + "headers": [ + "IP 地址", + "归属地", + "关联用户", + "注册用户", + "登录用户", + "正常", + "已封禁", + "最近活动", + "操作", + "IP 地址", + "归属地", + "关联用户", + "注册用户", + "登录用户", + "正常", + "已封禁", + "最近活动", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "61.153.184.95", + "浙江 绍兴", + "3多账号", + "0", + "3", + "3", + "0", + "2026-08-02 22:27:59", + "用户列表" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "IPv4 或 IPv6", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "用户列表" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/17-ip-account-stats.queried.json b/docs/admin-adfahi-research/17-ip-account-stats.queried.json new file mode 100644 index 0000000..95622be --- /dev/null +++ b/docs/admin-adfahi-research/17-ip-account-stats.queried.json @@ -0,0 +1,134 @@ +{ + "label": "IP 账号统计", + "slug": "17-ip-account-stats", + "route": "/ip-user-stats", + "url": "https://admin.adfahi.cn/ip-user-stats", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/17-ip-account-stats.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\nIP 账号统计\nIP\n查询\n重 置\nIP 地址\t归属地\t关联用户\t注册用户\t登录用户\t正常\t已封禁\t最近活动\t操作\n\nIP 地址\n\t\n归属地\n\t\n关联用户\n\t\n注册用户\n\t\n登录用户\n\t\n正常\n\t\n已封禁\n\t\n最近活动\n\t\n操作\n\n61.153.184.95\t浙江 绍兴\t\n3\n多账号\n\t0\t3\t3\t0\t2026-08-02 22:27:59\t\n用户列表\n共 1 个 IP\n1\n20 条/页", + "tables": [ + { + "headers": [ + "IP 地址", + "归属地", + "关联用户", + "注册用户", + "登录用户", + "正常", + "已封禁", + "最近活动", + "操作", + "IP 地址", + "归属地", + "关联用户", + "注册用户", + "登录用户", + "正常", + "已封禁", + "最近活动", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "61.153.184.95", + "浙江 绍兴", + "3多账号", + "0", + "3", + "3", + "0", + "2026-08-02 22:27:59", + "用户列表" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "IPv4 或 IPv6", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "用户列表", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/18-login-failures.json b/docs/admin-adfahi-research/18-login-failures.json new file mode 100644 index 0000000..95510b3 --- /dev/null +++ b/docs/admin-adfahi-research/18-login-failures.json @@ -0,0 +1,156 @@ +{ + "label": "登录失败日志", + "slug": "18-login-failures", + "route": "/login-failure-logs", + "url": "https://admin.adfahi.cn/login-failure-logs", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/18-login-failures.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n登录失败日志\n类型\n全部\n登录账号\nIP\n开始时间\n结束时间\n查询\n重 置\n类型\t登录账号\t失败原因\tIP 地址\tUser-Agent\t时间\t操作\n\n类型\n\t\n登录账号\n\t\n失败原因\n\t\nIP 地址\n\t\nUser-Agent\n\t\n时间\n\t\n操作\n\n用户\t13800001111\t密码错误\t106.119.66.106\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\t2026-08-01 01:31:20\t\n解锁\n\n用户\t13800001111\t密码错误\t61.153.184.95\tokhttp/4.12.0\t2026-07-31 22:44:36\t\n解锁\n1\n20 条/页", + "tables": [ + { + "headers": [ + "类型", + "登录账号", + "失败原因", + "IP 地址", + "User-Agent", + "时间", + "操作", + "类型", + "登录账号", + "失败原因", + "IP 地址", + "User-Agent", + "时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "用户", + "13800001111", + "密码错误", + "106.119.66.106", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0", + "2026-08-01 01:31:20", + "解锁" + ], + [ + "用户", + "13800001111", + "密码错误", + "61.153.184.95", + "okhttp/4.12.0", + "2026-07-31 22:44:36", + "解锁" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解锁" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解锁" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/18-login-failures.queried.json b/docs/admin-adfahi-research/18-login-failures.queried.json new file mode 100644 index 0000000..86001b8 --- /dev/null +++ b/docs/admin-adfahi-research/18-login-failures.queried.json @@ -0,0 +1,172 @@ +{ + "label": "登录失败日志", + "slug": "18-login-failures", + "route": "/login-failure-logs", + "url": "https://admin.adfahi.cn/login-failure-logs", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/18-login-failures.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n操作日志\n用户账号审计\n群事件审计\n管理员登录\n用户登录\nIP 账号统计\n登录失败日志\n系统设置\nheiyue\nheiyue\n账户操作\n数据概览\n登录失败日志\n类型\n全部\n登录账号\nIP\n开始时间\n结束时间\n查询\n重 置\n类型\t登录账号\t失败原因\tIP 地址\tUser-Agent\t时间\t操作\n\n类型\n\t\n登录账号\n\t\n失败原因\n\t\nIP 地址\n\t\nUser-Agent\n\t\n时间\n\t\n操作\n\n用户\t13800001111\t密码错误\t106.119.66.106\tMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\t2026-08-01 01:31:20\t\n解锁\n\n用户\t13800001111\t密码错误\t61.153.184.95\tokhttp/4.12.0\t2026-07-31 22:44:36\t\n解锁\n1\n20 条/页", + "tables": [ + { + "headers": [ + "类型", + "登录账号", + "失败原因", + "IP 地址", + "User-Agent", + "时间", + "操作", + "类型", + "登录账号", + "失败原因", + "IP 地址", + "User-Agent", + "时间", + "操作" + ], + "idx": 0, + "rows": [ + [], + [ + "用户", + "13800001111", + "密码错误", + "106.119.66.106", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0", + "2026-08-01 01:31:20", + "解锁" + ], + [ + "用户", + "13800001111", + "密码错误", + "61.153.184.95", + "okhttp/4.12.0", + "2026-07-31 22:44:36", + "解锁" + ] + ] + } + ], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "全部", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "可选", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "查询", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "重 置", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解锁", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "解锁", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": true, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "DIV", + "text": "20 条/页", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/19-private-domain-settings.json b/docs/admin-adfahi-research/19-private-domain-settings.json new file mode 100644 index 0000000..533c309 --- /dev/null +++ b/docs/admin-adfahi-research/19-private-domain-settings.json @@ -0,0 +1,67 @@ +{ + "label": "私域设置", + "slug": "19-private-domain-settings", + "route": "/tenant-settings", + "url": "https://admin.adfahi.cn/tenant-settings", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/19-private-domain-settings.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\n私域设置\n安全策略\nheiyue\nheiyue\n账户操作\n数据概览\n私域设置\n私域设置 - 黑月测试\n消息字符上限\n范围 1~5000,仅允许调低\n图片大小上限(MB)\n范围 1~1000 MB\n文件大小上限(MB)\n范围 1~1000 MB,默认 50 MB\n最大撤回消息时间(分钟)\n群主/管理员撤回自己或他人消息的最长时限,范围 1~1440 分钟;普通用户撤回自己的消息固定为 2 分钟\n是否开启手机验证码\n开启\n关闭\n关闭后,注册仅支持邀请码,短信登录与忘记密码不可用\n保存设置", + "tables": [], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "开启关闭" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "保存设置" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/19-private-domain-settings.queried.json b/docs/admin-adfahi-research/19-private-domain-settings.queried.json new file mode 100644 index 0000000..52c50ae --- /dev/null +++ b/docs/admin-adfahi-research/19-private-domain-settings.queried.json @@ -0,0 +1,75 @@ +{ + "label": "私域设置", + "slug": "19-private-domain-settings", + "route": "/tenant-settings", + "url": "https://admin.adfahi.cn/tenant-settings", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/19-private-domain-settings.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\n私域设置\n安全策略\nheiyue\nheiyue\n账户操作\n数据概览\n私域设置\n私域设置 - 黑月测试\n消息字符上限\n范围 1~5000,仅允许调低\n图片大小上限(MB)\n范围 1~1000 MB\n文件大小上限(MB)\n范围 1~1000 MB,默认 50 MB\n最大撤回消息时间(分钟)\n群主/管理员撤回自己或他人消息的最长时限,范围 1~1440 分钟;普通用户撤回自己的消息固定为 2 分钟\n是否开启手机验证码\n开启\n关闭\n关闭后,注册仅支持邀请码,短信登录与忘记密码不可用\n保存设置", + "tables": [], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "5000" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "10" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "50" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "2" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "开启关闭", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "保存设置", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/20-security-policy.json b/docs/admin-adfahi-research/20-security-policy.json new file mode 100644 index 0000000..f175e84 --- /dev/null +++ b/docs/admin-adfahi-research/20-security-policy.json @@ -0,0 +1,157 @@ +{ + "label": "安全策略", + "slug": "20-security-policy", + "route": "/security-settings", + "url": "https://admin.adfahi.cn/security-settings", + "title": "鸽达达管理后台", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots/20-security-policy.png", + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\n私域设置\n安全策略\nheiyue\nheiyue\n账户操作\n数据概览\n安全策略\n用户安全\n验证码触发次数\n连续密码错误该次数后要求验证码(1~10)\n密码最小位数\n用户密码最少字符数(6~32)\n强制包含字母\n开启后密码必须包含英文字母\n注册登录管控\n用户注册功能\n关闭后用户端将隐藏注册入口\n修改密码功能\n关闭后用户无法修改密码,不影响后台重置\n用户登录功能\n关闭后用户无法登录,不影响已登录用户\nWeb 端多端登录\n开启后同一账号可在多个浏览器标签页同时登录\n聊天消息管控\n全局好友功能\n关闭后无法搜索和添加好友\n全局群消息\n关闭后所有群无法发送消息\n全局私聊消息\n关闭后所有私聊无法发送消息\n群聊发言间隔(秒)\n0 表示不限制;1~1000 表示同一群内两次发言的最小间隔\n群组功能管控\n群号搜索功能\n关闭后用户端隐藏群号和群搜索入口\n群创建功能\n关闭后任何人无法新建群聊\n群解散功能\n关闭后任何人无法解散群聊,不影响后台管理员\n群管理员功能\n关闭后任何人无法设置群管理员\n是否显示群人数\n关闭后用户端查看群资料时不显示群总人数\n其他功能开关\n允许用户注销账号\n关闭后用户端将隐藏注销账号按钮\nIP 访问控制\n注册 IP 黑名单\n留空表示不限制;每行一个 IPv4、IPv6 或 CIDR。命中后无法注册及获取注册短信验证码。\n登录 IP 黑名单\n留空表示不限制;命中后无法密码登录、短信登录及刷新 Token。与用户分组登录白名单独立,黑名单优先。\n高级功能\n消息已读状态\n开启后显示消息已读/未读标记\n保存设置", + "tables": [], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "203.0.113.10\n10.0.0.0/8", + "tag": "TEXTAREA", + "text": "" + }, + { + "disabled": null, + "placeholder": "198.51.100.0/24", + "tag": "TEXTAREA", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "保存设置" + } + ], + "resources": [] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/20-security-policy.queried.json b/docs/admin-adfahi-research/20-security-policy.queried.json new file mode 100644 index 0000000..ffd3870 --- /dev/null +++ b/docs/admin-adfahi-research/20-security-policy.queried.json @@ -0,0 +1,180 @@ +{ + "label": "安全策略", + "slug": "20-security-policy", + "route": "/security-settings", + "url": "https://admin.adfahi.cn/security-settings", + "screenshot": "/Users/youshunlai/go_workspace/im_manager/docs/admin-adfahi-research/screenshots-queried/20-security-policy.png", + "queried": true, + "text": "鸽达达管理后台\n数据概览\n用户与群组\n内容审核\n日志审计\n系统设置\n私域设置\n安全策略\nheiyue\nheiyue\n账户操作\n数据概览\n安全策略\n用户安全\n验证码触发次数\n连续密码错误该次数后要求验证码(1~10)\n密码最小位数\n用户密码最少字符数(6~32)\n强制包含字母\n开启后密码必须包含英文字母\n注册登录管控\n用户注册功能\n关闭后用户端将隐藏注册入口\n修改密码功能\n关闭后用户无法修改密码,不影响后台重置\n用户登录功能\n关闭后用户无法登录,不影响已登录用户\nWeb 端多端登录\n开启后同一账号可在多个浏览器标签页同时登录\n聊天消息管控\n全局好友功能\n关闭后无法搜索和添加好友\n全局群消息\n关闭后所有群无法发送消息\n全局私聊消息\n关闭后所有私聊无法发送消息\n群聊发言间隔(秒)\n0 表示不限制;1~1000 表示同一群内两次发言的最小间隔\n群组功能管控\n群号搜索功能\n关闭后用户端隐藏群号和群搜索入口\n群创建功能\n关闭后任何人无法新建群聊\n群解散功能\n关闭后任何人无法解散群聊,不影响后台管理员\n群管理员功能\n关闭后任何人无法设置群管理员\n是否显示群人数\n关闭后用户端查看群资料时不显示群总人数\n其他功能开关\n允许用户注销账号\n关闭后用户端将隐藏注销账号按钮\nIP 访问控制\n注册 IP 黑名单\n留空表示不限制;每行一个 IPv4、IPv6 或 CIDR。命中后无法注册及获取注册短信验证码。\n登录 IP 黑名单\n留空表示不限制;命中后无法密码登录、短信登录及刷新 Token。与用户分组登录白名单独立,黑名单优先。\n高级功能\n消息已读状态\n开启后显示消息已读/未读标记\n保存设置", + "tables": [], + "controls": [ + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "3" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "INPUT", + "text": "", + "value": "0" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "203.0.113.10\n10.0.0.0/8", + "tag": "TEXTAREA", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "198.51.100.0/24", + "tag": "TEXTAREA", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "", + "value": "" + }, + { + "disabled": null, + "placeholder": "", + "tag": "BUTTON", + "text": "保存设置", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/admin-adfahi-research/Admin-Server改造文档.md b/docs/admin-adfahi-research/Admin-Server改造文档.md new file mode 100644 index 0000000..b189d22 --- /dev/null +++ b/docs/admin-adfahi-research/Admin-Server改造文档.md @@ -0,0 +1,409 @@ +# Admin Server 改造文档:对齐鸽达达管理后台 + +本文基于 `docs/admin-adfahi-research/需求文档.md`、当前 `im_manager` 代码、`im_server` 文档与本地 MySQL 数据核对结果编写。目标是在 HotGo Admin Server 中复刻鸽达达后台的 20 个页面,并保持当前系统“Admin 只直连 hotgo 控制库,IM Server 只直连租户业务库”的边界。 + +> 实施状态(2026-09-07):Admin 第二批已完成 `imDashboard`、`imPrivateMessage`、`imGroupMessage`、`imUserLoginHistory`、`imLoginFailureLog` 的 API、service、logic、controller、路由、Vue 页面、字典、导出和菜单权限。消息隐藏/删除、失败日志封禁用户使用 Kafka Admin Command;失败 IP 加黑复用安全策略。对应菜单 SQL 为 `server/storage/data/generate/im_adfahi_batch2_menu.sql`,已在本地 `hotgo` 执行。上述查询仍依赖 IM Server 本文约定的 internal-query;本地 `tinode` 尚无 `im_user_login_logs`、`im_login_failure_logs`,需在 IM Server 批次建表并接入日志采集后才会产生真实数据。 + +## 1. 改造边界 + +Admin Server 负责后台菜单、权限、页面、查询聚合、运营配置、Kafka 管理命令投递、命令结果展示与后台审计落库。业务 IM 数据不直接跨库读写,用户、群、消息、文件、反馈、登录等租户数据通过 IM Server 内部只读接口 `/internal/admin-query/tenants/{tenant_code}/...` 查询;用户封禁、群治理、消息处置、文件处置等写操作通过 Kafka Admin Command 下发。 + +当前本地库观察: + +| 数据库 | 现状 | +| --- | --- | +| `hotgo` | 当前有 66 张表,已存在 `im_admin_commands`、`im_admin_operation_audit`、`im_admin_outbox`、`im_registration_invite_codes`、`im_external_config`、`im_file_policy`,并已新增策略分组、敏感词、安全策略和宵禁控制表。 | +| `tinode` | 当前有 73 张表,已有 `im_users`、`im_topics`、`im_subscriptions`、`im_messages`、`im_group_messages`、`im_group_settings`、`im_fileuploads`、`im_feedback_submissions` 等 IM 业务表。 | +| 联调状态 | `hotgo.im_tenant` 已补默认租户映射(`id=1`、`code=default`);`tinode` 尚无用户登录历史和登录失败日志表,对应页面待 IM Server 批次补齐数据源。 | + +敏感配置说明:`hg_sys_config` 中已有声网、阿里云、OSS、短信等配置项。文档与页面只能展示脱敏值,后续应补充加密存储、轮换、审计与权限控制。 + +## 2. 当前 Admin 能力盘点 + +已有后台 API 模块: + +| 模块 | 主要能力 | 复用结论 | +| --- | --- | --- | +| `imUser` | 用户列表、详情、设备、重置密码、状态、封禁/解封、冻结/解冻、踢设备、全端登出、注销、删除 | 可作为“用户管理”的主体,补鸽达达字段、筛选项、批量操作与审计页联动。 | +| `imGroup` | 群列表、详情、成员、群治理、资料更新、解散、禁言、成员禁言/移除、入群申请、邀请、公告、文件、管理日志 | 可覆盖“群组管理”和“群成员管理”主体,需补独立菜单页与鸽达达状态/字段映射。 | +| `imInvite` | 邀请码列表、新增、批量新增、批量查看、撤销、批量撤销、导出 | 可覆盖“邀请码管理”,但邀请码格式与鸽达达需求存在差异。 | +| `imFile` | 文件列表、详情、隔离、删除、清理孤儿文件 | 可覆盖“文件管理”,需补预览/下载链接、审核枚举、内容风险字段。 | +| `imAuditLog` | 操作日志列表、详情、导出 | 可覆盖后台操作日志的一部分,需扩展为用户账号审计、群事件审计等专题页。 | +| `imKafkaCommand`、`imDeadLetter`、`imOutbox`、`imKafkaAudit` | Kafka 命令、死信、出站事件、审计事件 | 保留为运维治理菜单,不直接出现在鸽达达 20 个业务页中。 | +| `imRegistration`、`imExternalConfig`、`imFilePolicy`、`imFeedbackConfig`、`imMailboxConfig`、`imNotification`、`imAppRelease` | 注册、第三方、文件策略、反馈、邮箱、通知、版本配置 | 可组合成“私域设置”和“安全策略”,需要按鸽达达页面重组展示。 | + +已有菜单已包含 `IM管理`、`注册邀请码`、`用户列表`、`群组列表`、`文件治理`、`Kafka命令`、`Kafka死信`、`Kafka审计事件`、`租户管理` 等。缺少与鸽达达完全一致的“单聊消息、群聊消息、敏感词库、用户反馈、用户登录、IP账号统计、登录失败日志”等前台页面。 + +## 3. 页面级改造 + +### 3.1 数据概览 + +新增或改造 `imDashboard` 页面。顶部统计按需求文档展示总用户数、活跃用户、总群组数、总会话数、总消息数、今日消息;趋势表展示最近 7 天用户增长、消息增长。现有 IM Server 内部接口已有 `/internal/admin-query/tenants/{tenant}/dashboard/summary`,但仅返回 `dead_kafka`、`pending_business`、`pending_group`、`pending_realname`、`received_abuse`、`received_feedback`、`active_groups`、`suspended_users`,不足以还原鸽达达数据概览。 + +Admin 改造: + +| 项 | 方案 | +| --- | --- | +| API | 新增 `/imDashboard/overview`、`/imDashboard/trends`。 | +| 数据源 | 控制库租户信息来自 `im_tenant`;统计与趋势来自 IM internal query。 | +| 字段 | `totalUsers`、`activeUsers`、`totalGroups`、`totalConversations`、`totalMessages`、`todayMessages`、`date`、`userCount`、`messageCount`。 | +| 按钮 | 页面无写操作;支持租户切换、日期范围查询、刷新。 | +| 缺口 | IM Server 需补 dashboard 聚合项与趋势接口;Admin 需补租户为空时的默认租户兜底提示。 | + +### 3.2 用户管理 + +复用 `imUser`,页面按鸽达达字段重排。列表数据通过 IM internal `users` 查询,详情通过 `/users/{id}`,设备通过 `/users/{id}/devices`。当前 Admin 已有 `/imUser/list`、`/imUser/view`、`/imUser/devices`、`/imUser/ban`、`/imUser/unban`、`/imUser/freeze`、`/imUser/unfreeze`、`/imUser/deviceKick`、`/imUser/logoutAll`、`/imUser/accountCancel`、`/imUser/accountDelete`、`/imUser/resetPassword`。 + +字段说明: + +| 字段 | 来源 | 说明 | +| --- | --- | --- | +| `user_id` | `tinode.im_users.id` 编码后 | 用户唯一 ID,列表、详情、审计联动主键。 | +| `username` | `im_users.username` | 登录账号。 | +| `phone` | `im_users.phone` | 手机号,页面默认脱敏,导出需权限。 | +| `nickname` | `im_user_profiles.nickname` | 昵称,支持模糊查询。 | +| `public_account` | `im_user_profiles.public_account` | 对外账号/靓号。 | +| `state` | `im_users.state` | Tinode 用户状态。建议映射为正常、封禁、冻结、注销、删除。 | +| `last_seen` | `im_users.last_seen` | 最后在线/活跃时间。 | +| `created_at` | `im_users.created_at` | 注册时间。 | +| `credentials` | IM internal 聚合 | 认证方式摘要,不展示密钥。 | + +按钮功能: + +| 按钮 | 已有接口 | 功能 | +| --- | --- | --- | +| 查询 | `/imUser/list` | 按用户 ID、昵称、账号、手机号、状态、注册时间、最后活跃时间筛选。 | +| 重置 | 前端表单重置 | 清空筛选项并重新查询。 | +| 查看 | `/imUser/view` | 展示基础信息、资料、设备、最近审计事件。 | +| 封禁/解封 | `/imUser/ban`、`/imUser/unban` | Kafka 命令 `user.ban` / `user.unban`,需填写原因和过期时间。 | +| 冻结/解冻 | `/imUser/freeze`、`/imUser/unfreeze` | Kafka 命令 `user.freeze` / `user.unfreeze`。 | +| 踢设备 | `/imUser/deviceKick` | 指定设备下线。 | +| 全端登出 | `/imUser/logoutAll` | 下发 `user.logout_all`。 | +| 注销/删除 | `/imUser/accountCancel`、`/imUser/accountDelete` | 高危操作,二次确认并写审计。 | + +枚举说明:`state` 当前来自 Tinode 状态值,Admin 应新增统一字典 `im_user_state`,至少包含正常、封禁、冻结、注销、删除。封禁/冻结原因不应写死在前端,可复用“安全策略”中的处置原因配置。 + +### 3.3 用户分组 + +当前缺少等价模块,需要新增 `imUserPolicyGroup`。该页面不是组织部门,而是账号策略分组,用于控制注册、登录、私聊、建群、入群、发文件、发消息、IP 限制等策略。 + +Admin 改造: + +| 项 | 方案 | +| --- | --- | +| 后端模块 | `api/admin/imuserpolicygroup`、`controller/admin/sys/im_user_policy_group.go`、`logic/sys/im_user_policy_group.go`、`model/input/sysin/im_user_policy_group.go`。 | +| 控制表 | 新增 `im_user_policy_groups`、`im_user_policy_group_members`。 | +| API | `/imUserPolicyGroup/list`、`/view`、`/edit`、`/delete`、`/status`、`/memberList`、`/memberAdd`、`/memberRemove`。 | +| 同步 | 策略变更写 `im_admin_outbox`,发布 Kafka config event;IM Server 缓存策略并执行。 | +| 页面按钮 | 查询、新增、编辑、删除、启用/禁用、成员管理、导出。 | + +字段说明: + +| 字段 | 说明 | +| --- | --- | +| `group_code` | 分组编码,唯一,不可编辑。 | +| `group_name` | 分组名称。 | +| `priority` | 命中多个分组时的优先级,数字越大优先级越高。 | +| `status` | 启用、禁用。 | +| `policy_json` | 策略 JSON,包含消息、好友、群、文件、登录等开关与限额。 | +| `ip_whitelist`、`ip_blacklist` | IP 白名单/黑名单,支持 CIDR。 | +| `remark` | 备注。 | + +好友策略必须覆盖移动端所有加好友入口,不能只依赖前端隐藏。分组策略中至少包含 `allow_add_friend`、`direct_add_friend`、`max_friend_count`、`allow_view_profile_when_friend_disabled`、`allow_qrcode_add_friend`、`allow_group_member_profile`。当全局或分组禁止加好友时,从搜索、通讯录推荐、群成员列表、个人名片、二维码解析、邀请注册自动加好友、转发目标联系人等入口都必须由后端返回禁止或脱敏数据。 + +枚举说明:`status` 使用 `sys_normal_disable`;策略开关使用布尔值;策略动作建议统一枚举 `allow`、`deny`、`challenge`、`audit_only`。好友入口枚举建议 `search`、`profile_card`、`group_member`、`qrcode`、`invite_auto_add`、`recommendation`。 + +### 3.4 群组管理 + +复用 `imGroup`。列表通过 IM internal `groups` 查询,详情通过 `/groups/{topic}`,现有 Admin 已有群资料、治理、成员、申请、邀请、公告、文件、日志等接口。 + +字段说明: + +| 字段 | 来源 | 说明 | +| --- | --- | --- | +| `topic` | `im_topics.name` | 群 Topic,等价群号/群唯一标识。 | +| `group_name` | `im_group_settings.group_name` | 群名称。 | +| `owner_id` | `im_topics.owner` | 群主。 | +| `group_status` | `im_group_settings.group_status` | 群业务状态。 | +| `state` | `im_topics.state` | Tinode topic 状态。 | +| `business_member_count` | `im_group_settings.business_member_count` | 成员数。 | +| `join_approval` | `im_group_settings.join_approval` | 入群审批开关。 | +| `mute_all` | `im_group_settings.mute_all` | 全员禁言。 | +| `created_at`、`updated_at` | topic/settings | 创建与更新时间。 | + +按钮功能: + +| 按钮 | 已有接口 | 功能 | +| --- | --- | --- | +| 查询 | `/imGroup/list` | 按群号、群名、群主、状态、创建时间查询。 | +| 查看 | `/imGroup/view` | 群资料、成员、公告、邀请、文件、治理配置汇总。 | +| 编辑资料 | `/imGroup/profileUpdate` | Kafka `group.profile.update`。 | +| 治理设置 | `/imGroup/governanceUpdate` | Kafka `group.governance.update`,控制入群审批、全员禁言、成员上传文件、成员私聊、成员邀请。 | +| 解散 | `/imGroup/dissolve` | 高危操作,二次确认。 | +| 禁言/解禁言 | `/imGroup/mute`、`/imGroup/unmute` | 群级禁言。 | +| 成员/申请/邀请/公告/文件/日志 | 已有子接口 | 打开子面板或跳转专题页。 | + +枚举说明:新增 `im_group_status`,建议值 `active` 正常、`muted` 禁言、`suspended` 封停、`dissolved` 已解散。当前 IM 命令未看到明确 `group.ban`/`group.unban`,如鸽达达要求群封停,需要新增 Kafka 命令或通过治理状态统一处理。 + +### 3.5 群成员管理 + +复用 `/imGroup/memberList` 与成员治理命令,新增独立菜单页 `imGroupMember`,默认要求输入群号或从群详情跳转携带 `topic`。 + +字段说明: + +| 字段 | 说明 | +| --- | --- | +| `topic` | 群 Topic。 | +| `user_id` | 成员用户 ID。 | +| `display_name`/`nickname` | 群内展示名或用户昵称。 | +| `role` | 群主、管理员、普通成员。 | +| `status` | 正常、禁言、已移除。 | +| `muted_until` | 禁言截止时间。 | +| `joined_at` | 入群时间。 | + +按钮功能:查询、查看用户、设为管理员、取消管理员、转让群主、成员禁言、解除禁言、移除成员。现有接口分别为 `/imGroup/adminSet`、`/adminRemove`、`/ownerTransfer`、`/memberMute`、`/memberUnmute`、`/memberRemove`。 + +枚举说明:`role` 新增字典 `im_group_member_role`:`owner` 群主、`admin` 管理员、`member` 成员;成员状态新增 `im_group_member_status`。 + +### 3.6 邀请码管理 + +复用 `imInvite`。本地 `hotgo.im_registration_invite_codes` 有样例数据,批次表为空。当前需求文档按鸽达达页面描述 6 位数字邀请码;历史 IM 文档曾描述 5 位 ASCII 字母邀请码,二者冲突。若以复刻鸽达达为目标,应统一改为 6 位数字,Admin 生成、IM 注册校验、数据库唯一索引与错误文案同步调整。 + +字段说明:`code` 邀请码、`batch_id` 批次、`status` 状态、`max_uses` 最大使用次数、`used_count` 已使用次数、`expires_at` 过期时间、`created_by` 创建管理员、`created_at` 创建时间、`revoked_at` 撤销时间。 + +按钮功能:查询、新增、批量生成、批量查看、撤销、批量撤销、导出。接口已有 `/imInvite/list`、`/create`、`/batchCreate`、`/batchView`、`/revoke`、`/batchRevoke`、`/export`。 + +枚举说明:新增或确认 `im_invite_status`:`unused` 未使用、`used` 已用完、`expired` 已过期、`revoked` 已撤销。 + +### 3.7 单聊消息 + +当前 Admin 缺少独立消息管理模块,需要新增 `imPrivateMessage`。IM Server 已有消息表 `im_messages`,但 internal admin query 目前只支持 `/messages/{topic}/{seq}` 单条查询,缺少按会话分页检索单聊消息的后台列表接口。 + +Admin 改造: + +| 项 | 方案 | +| --- | --- | +| API | `/imPrivateMessage/list`、`/view`、`/hide`、`/delete`、`/export`。 | +| 数据源 | 新增 IM internal query:`/tenants/{tenant}/private-messages` 或 `/messages?kind=p2p`。 | +| 写操作 | 通过 Kafka 命令新增 `message.hide`、`message.delete` 或复用 IM 已有 recall/delete 能力封装为后台命令。 | +| 页面按钮 | 查询、查看上下文、隐藏、删除、导出。 | + +字段说明:`topic` 会话 Topic、`seq_id` 消息序号、`from_user_id` 发送者、`to_user_id` 接收者、`content_type` 内容类型、`content` 内容摘要、`status` 状态、`created_at` 发送时间、`updated_at` 更新时间。 + +枚举说明:`content_type` 建议 `text`、`image`、`file`、`audio`、`video`、`card`、`system`;`status` 建议 `normal`、`recalled`、`hidden`、`deleted`。 + +### 3.8 群聊消息 + +新增 `imGroupMessage` 页面。IM Server 本地有 `im_group_messages` 表,需求需要按群号、发送者、时间、内容关键字筛选。 + +Admin 改造:新增 `/imGroupMessage/list`、`/view`、`/hide`、`/delete`、`/export`。IM Server 补 internal query `group-messages`,支持 `topic`、`sender_user_id`、`content_type`、`status`、`created_at_start/end`、`keyword`。 + +字段、按钮、枚举与单聊消息一致,额外展示 `group_name`、`member_role`、`at_users`、`reply_to_seq`。 + +### 3.9 敏感词库 + +当前 Admin 与 IM Server 未发现完整敏感词库模块,需要新增。Admin 维护词库,IM Server 在消息发送、昵称/群名修改、反馈/举报提交时执行匹配。 + +Admin 改造: + +| 项 | 方案 | +| --- | --- | +| 表 | `im_sensitive_words`、`im_sensitive_word_groups`、`im_sensitive_word_hits` 可按需拆分。 | +| API | `/imSensitiveWord/list`、`/view`、`/edit`、`/delete`、`/status`、`/import`、`/export`。 | +| 同步 | 词库变更发布 config event 或 cache invalidate,IM Server 热加载。 | +| 按钮 | 查询、新增、编辑、删除、启用/禁用、导入、导出。 | + +字段说明:`word` 敏感词、`match_type` 匹配类型、`scene` 生效场景、`action` 命中动作、`replacement` 替换文本、`level` 风险等级、`status` 状态。 + +枚举说明:`match_type` 为 `exact` 精确、`contains` 包含、`regex` 正则;`scene` 为 `message` 消息、`profile` 资料、`group` 群资料、`feedback` 反馈;`action` 为 `block` 拦截、`replace` 替换、`review` 人审、`audit_only` 仅记录。 + +### 3.10 文件管理 + +复用 `imFile`,补齐鸽达达文件字段和预览能力。IM internal 已支持 `files`、`files/{id}`、`files/{id}/links`。本地 `tinode.im_fileuploads` 有数据,`im_group_files` 暂无数据。 + +字段说明:`file_id` 文件 ID、`user_id` 上传者、`topic` 所属会话、`mime_type` MIME、`size` 文件大小、`location` 存储位置、`etag` 校验、`status` 状态、`review_status` 审核状态、`risk_level` 风险等级、`created_at` 上传时间。 + +按钮功能:查询、查看、预览/下载、隔离、删除、清理孤儿文件。现有接口 `/imFile/quarantine`、`/imFile/delete`、`/imFile/cleanupOrphan` 可复用;预览/下载建议由 Admin 获取短期签名 URL,不直接暴露 OSS 原始地址。 + +枚举说明:现有字典包括 `im_storage_provider=oss`、`im_file_review_provider`、`im_file_review_status(pending/running/clean/risky/failed)`、`im_file_review_task_type(content/malware)`;还需补 `im_file_status`:`normal` 正常、`quarantined` 已隔离、`deleted` 已删除。 + +### 3.11 用户反馈 + +IM Server 已有 `im_feedback_submissions` 和 internal resource `feedback-submissions`,Admin 目前有反馈配置和工单模块,但缺少完全按鸽达达展示的反馈列表页。建议新增 `imFeedback` 或改造 `imWorkOrder` 作为反馈处理页。 + +字段说明:`feedback_id` 反馈 ID、`user_id` 用户、`category` 分类、`content` 内容、`contact` 联系方式、`file_ids` 附件、`client_context` 客户端环境、`include_diagnostics` 是否含诊断、`status` 状态、`created_at`、`updated_at`。 + +按钮功能:查询、查看、标记处理中、标记已处理、关闭、导出。写操作建议先落 Admin 工单状态,再通过 Kafka 或 IM internal 写接口回写业务状态。 + +枚举说明:`status` 建议 `received` 已收到、`processing` 处理中、`resolved` 已解决、`closed` 已关闭;`category` 来自 `im_feedback_config.categories`,禁止前端硬编码。 + +### 3.12 操作日志 + +复用 `imAuditLog` 与 `im_admin_operation_audit`。本地该表已有 45 条记录。页面应覆盖后台管理员对 IM 资源的所有操作,包括用户、群、消息、文件、配置、邀请码、敏感词。 + +字段说明:`id`、`request_id`、`admin_id`、`admin_name`、`action`、`target_type`、`target_id`、`before_data`、`after_data`、`ip`、`user_agent`、`result`、`created_at`。 + +按钮功能:查询、查看详情、导出。详情页需展示变更前后 JSON,敏感字段脱敏。 + +枚举说明:`target_type` 建议 `user`、`group`、`message`、`file`、`invite`、`config`、`sensitive_word`;`result` 为 `success`、`failed`、`pending`。 + +### 3.13 用户账号审计 + +新增专题页 `imUserAccountAudit`,从 Admin 操作审计、IM `audit-logs`、Kafka 命令结果中聚合用户生命周期事件。 + +字段说明:`event_id`、`user_id`、`action`、`operator_type`、`operator_id`、`reason`、`ip`、`device_id`、`before_state`、`after_state`、`created_at`。 + +按钮功能:查询、查看、跳转用户详情、导出。动作枚举建议包含注册、登录、登出、登录失败、改密、重置密码、封禁、解封、冻结、解冻、注销、删除、踢设备。 + +缺口:IM Server 当前 `im_audit_logs` 本地为空,需要补登录、失败、状态变更等事件写入。 + +### 3.14 群事件审计 + +可复用 `imGroup/adminLogList`、`outbox/group-events` 和 Admin audit,新增独立页面 `imGroupEventAudit`。 + +字段说明:`event_id`、`topic`、`group_name`、`operator_id`、`target_user_id`、`action`、`object_type`、`object_id`、`before_data`、`after_data`、`created_at`。 + +按钮功能:查询、查看、跳转群详情、导出。动作枚举建议包含建群、解散、修改资料、修改治理设置、入群申请、审批通过、审批拒绝、成员禁言、解除禁言、移除成员、设管理员、取消管理员、转让群主、公告发布/删除、文件删除。 + +### 3.15 管理员登录历史 + +复用 HotGo 系统登录日志能力,不需要 IM Server 改造。若现有登录日志字段不足,补 Admin 聚合接口 `/imAdminLoginHistory/list` 或直接配置菜单指向系统日志页面。 + +字段说明:管理员 ID、账号、昵称、登录 IP、地区、浏览器、系统、登录结果、失败原因、登录时间。 + +按钮功能:查询、查看、导出。枚举 `result` 为成功、失败。 + +### 3.16 用户登录历史 + +当前缺少完整用户登录历史。IM Server 路由中未看到客户端 `/v1/im/auth/logout` 或独立 session 管理路由;本地 `im_devices` 为空,不能满足鸽达达登录历史。 + +Admin 改造:新增 `imUserLoginHistory` 页面,查询 IM internal `user-login-logs`。字段包括 `user_id`、`phone`、`login_type`、`device_id`、`platform`、`ip`、`region`、`user_agent`、`result`、`login_at`、`logout_at`。 + +枚举说明:`login_type` 为 `password`、`sms`、`token_refresh`、`third_party`;`platform` 为 `ios`、`android`、`web`、`desktop`;`result` 为 `success`、`failed`。 + +### 3.17 IP 账号统计 + +新增 `imIpAccountStats` 页面。数据由 IM Server 基于注册 IP、登录 IP、失败登录 IP 聚合生成,Admin 只查询展示。 + +字段说明:`ip`、`region`、`user_count`、`register_count`、`login_count`、`failure_count`、`first_seen_at`、`last_seen_at`、`risk_level`。 + +按钮功能:查询、查看关联账号、导出、加入黑名单。加入黑名单写入“安全策略”或“用户分组”策略并同步 IM Server。 + +枚举说明:`risk_level` 为 `low`、`medium`、`high`;黑名单动作 `block_login`、`challenge`、`audit_only`。 + +### 3.18 登录失败日志 + +新增 `imLoginFailureLog` 页面。IM Server 需要记录认证失败事件,包含错误原因、IP、设备、账号、风控命中信息。 + +字段说明:`id`、`account`、`user_id`、`failure_type`、`reason`、`ip`、`region`、`device_id`、`platform`、`user_agent`、`attempt_at`。 + +按钮功能:查询、查看、导出、加入 IP 黑名单、封禁账号。封禁账号复用 `/imUser/ban`。 + +枚举说明:`failure_type` 为 `password_error`、`code_error`、`account_not_found`、`account_disabled`、`rate_limited`、`risk_blocked`。 + +### 3.19 私域设置 + +以现有 `imRegistration`、`imExternalConfig`、`imFeedbackConfig`、`imMailboxConfig`、`imNotification`、`imAppRelease` 为基础,新增汇总页面 `imTenantSettings`,按鸽达达页面展示私域基础配置。 + +字段说明:租户标识、注册开关、邀请码开关、短信开关、默认策略分组、客服/反馈配置、上传配置、客户端功能开关、版本策略、更新时间。客户端功能开关中新增 `global_friend_enabled` 和 `message_curfew_enabled` 的只读摘要,便于运营在私域设置页看到当前私域是否允许加好友、是否存在宵禁;实际编辑入口放在“安全策略”。 + +按钮功能:查看、编辑、保存、测试第三方配置、发布配置。保存后写 `hg_sys_config` 或 `im_tenant_config`,并通过 Kafka config event 通知 IM Server 刷新缓存。 + +枚举说明:开关统一使用布尔;配置发布状态 `draft`、`published`、`failed`。 + +### 3.20 安全策略 + +新增或重组 `imSecurityPolicy` 页面,统一管理登录安全、密码策略、IP 风控、敏感词动作、文件审核、用户处置原因、消息处置策略、全局好友功能与消息宵禁。 + +字段说明:`password_policy`、`login_rate_limit`、`ip_blacklist`、`ip_whitelist`、`sensitive_word_policy`、`file_review_policy`、`realname_policy`、`risk_challenge_policy`、`global_friend_enabled`、`friend_disabled_policy`、`message_curfew_enabled`、`message_curfew_rules`。 + +全局好友功能说明: + +| 字段 | 说明 | +| --- | --- | +| `global_friend_enabled` | 全局是否允许加好友。关闭后所有移动端加好友入口都禁止。 | +| `friend_disabled_policy.hide_profile` | 禁止加好友时,群成员列表、个人名片、二维码解析是否隐藏个人资料。 | +| `friend_disabled_policy.block_qrcode` | 是否禁止通过二维码添加好友。 | +| `friend_disabled_policy.block_group_member_entry` | 是否禁止从群成员入口查看个人信息或发起添加。 | +| `friend_disabled_policy.blocked_entries` | 被禁用的移动端入口数组:`search`、`profile_card`、`group_member`、`qrcode`、`invite_auto_add`、`recommendation`。关闭全局好友功能时默认全部禁用。 | +| `friend_disabled_policy.message` | 移动端提示文案,例如“当前时段暂不允许添加好友”。 | + +宵禁功能说明: + +| 字段 | 说明 | +| --- | --- | +| `message_curfew_enabled` | 是否启用消息宵禁。 | +| `message_curfew_rules` | 宵禁规则数组,支持每天、工作日、周末、自定义日期。 | +| `start_time`、`end_time` | 宵禁开始和结束时间,支持跨天,例如 `18:00` 到次日 `09:00`。 | +| `scope` | 管控范围,默认至少包含 `private_message`,即单聊/私聊不能发消息;如产品确认,可扩展 `group_message`。 | +| `whitelist_user_ids`、`whitelist_group_ids` | 豁免用户或群,可选。 | +| `message` | 宵禁期间发送失败提示。 | + +按钮功能:查询当前策略、编辑、保存、发布、恢复默认、查看发布历史。保存必须写操作审计。 + +枚举说明:风控动作统一 `allow`、`challenge`、`block`、`audit_only`;文件审核动作 `allow`、`quarantine`、`delete`、`manual_review`;宵禁范围 `private_message` 单聊/私聊、`group_message` 群聊;日期类型 `daily` 每天、`weekday` 工作日、`weekend` 周末、`custom` 自定义。 + +## 4. 后端实现方案 + +按 HotGo 规范新增模块时,必须完成 API、controller、service、logic、model/input、router/genrouter、前端 API、views/model、index、edit。接口命名沿用当前项目风格,例如 `/imPrivateMessage/list`,不直接照搬鸽达达 `/admin/private-messages` 路径;如后续需要兼容外部路径,可单独加网关映射。 + +新增模块建议: + +| 优先级 | 模块 | 说明 | +| --- | --- | --- | +| P0 | `imDashboard` | 首页统计与趋势。 | +| P0 | `imPrivateMessage`、`imGroupMessage` | 单聊/群聊消息查询与处置。 | +| P0 | `imSensitiveWord` | 敏感词库与热更新。 | +| P0 | `imUserLoginHistory`、`imLoginFailureLog` | 用户登录与失败日志。 | +| P1 | `imUserPolicyGroup` | 用户策略分组。 | +| P1 | `imFeedback` | 用户反馈处理页。 | +| P1 | `imIpAccountStats` | IP 聚合与风控联动。 | +| P1 | `imUserAccountAudit`、`imGroupEventAudit` | 专题审计页。 | +| P2 | `imTenantSettings`、`imSecurityPolicy` | 运营配置整合页。 | + +Admin 查询 IM 数据统一走 `server/internal/library/iminternal` 或现有 `imadmin` 封装,不在 logic 层直接拼接外部 HTTP 细节。写操作统一创建 `im_admin_commands` 与 `im_admin_outbox`,等待 Kafka result 更新状态;页面展示 `pending/success/failed/dead`。 + +## 5. 数据库与字典 + +配套建表与兼容迁移脚本见 [`sql/Admin-Server改造.sql`](sql/Admin-Server改造.sql)。脚本面向 `hotgo` 控制库,只保存管理配置和发布版本,不保存租户业务消息。 + +控制库新增表优先放在 `hotgo`: + +| 表 | 用途 | +| --- | --- | +| `im_user_policy_groups` | 用户策略分组主表。 | +| `im_user_policy_group_members` | 用户与策略分组关系。 | +| `im_sensitive_word_groups` | 敏感词分组。 | +| `im_sensitive_words` | 敏感词主表。 | +| `im_security_policy` | 安全策略草稿/发布版本。 | +| `im_admin_query_snapshots` | 可选,用于缓存重型统计查询快照。 | + +字典由后端 `internal/consts` 注册,前端通过 `dict.loadOptions()` 加载。禁止在 `model.ts` 中硬编码枚举中文。 + +必补字典:`im_user_state`、`im_group_status`、`im_group_member_role`、`im_group_member_status`、`im_invite_status`、`im_message_content_type`、`im_message_status`、`im_sensitive_match_type`、`im_sensitive_scene`、`im_sensitive_action`、`im_feedback_status`、`im_login_type`、`im_login_failure_type`、`im_risk_level`、`im_policy_action`、`im_friend_entry`、`im_curfew_scope`、`im_curfew_date_type`。 + +## 6. 权限与审计 + +每个按钮权限与后端路由保持一致,例如 `/imPrivateMessage/delete`、`/imSensitiveWord/import`。高危操作包括用户删除、账号注销、群解散、消息删除、文件删除、IP 拉黑、安全策略发布,必须二次确认、记录原因、写 `im_admin_operation_audit`,并展示 Kafka 命令执行结果。 + +审计脱敏规则: + +| 类型 | 展示规则 | +| --- | --- | +| 手机号 | 中间 4 位脱敏。 | +| 身份证/实名 | 默认只展示脱敏字段。 | +| 配置密钥 | 只展示前后 3-4 位或完全隐藏。 | +| 消息内容 | 列表展示摘要,详情按权限展示全文。 | +| 文件地址 | 不展示原始存储地址,使用短期签名 URL。 | + +## 7. 联调与验收 + +验收顺序建议: + +1. 补齐 `hotgo.im_tenant` 与 `tinode` 默认租户映射,确保 Admin 能通过 tenant code 查询 IM internal。 +2. 完成数据概览、用户、群、邀请码、文件这些已有基础模块的字段对齐。 +3. 新增消息、敏感词、反馈、登录日志、IP 统计等缺口模块。 +4. 为 Kafka 命令增加幂等键、结果回写、失败重试、死信展示。 +5. 使用本地 `hotgo` 与 `tinode` 数据验证每个页面的查询、按钮、详情和导出。 + +最低验收标准:20 个菜单均可打开;查询按钮能返回真实或空态数据;表格字段与需求文档一致;所有枚举来自字典;高危按钮有二次确认、原因、审计与结果状态;敏感数据脱敏。 diff --git a/docs/admin-adfahi-research/IM-Server改造文档.md b/docs/admin-adfahi-research/IM-Server改造文档.md new file mode 100644 index 0000000..9f69b98 --- /dev/null +++ b/docs/admin-adfahi-research/IM-Server改造文档.md @@ -0,0 +1,390 @@ +# IM Server 改造文档:支撑鸽达达管理后台复刻 + +本文面向 `/Users/youshunlai/go_workspace/im_server` 的现状改造,目标是为 Admin Server 复刻鸽达达后台提供准确、稳定、可审计的数据与治理能力。当前 IM Server 已有 Tinode 风格实时链路、产品 HTTP API、独立租户库、Kafka 命令消费、配置事件消费、内部 Admin Query,但鸽达达 20 个后台页面仍需要补充若干查询资源、审计日志、风控策略与消息处置能力。 + +## 1. 当前实现现状 + +核心架构: + +| 能力 | 现状 | +| --- | --- | +| 实时链路 | `Session -> Hub -> Topic -> Store`。 | +| 客户端入口 | WebSocket `/v0/channels`,产品 HTTP `/v1/im/...`。 | +| 后台只读入口 | `/internal/admin-query/tenants/{tenant_code}/...`,要求内部签名与 capability。 | +| 后台写操作 | Kafka Admin Command 消费器 `startKafkaAdminCommandConsumer`。 | +| 配置同步 | Kafka config event、cache invalidate consumer 已存在。 | +| 消息转发 | `/v1/im/messages/forward` 已存在。 | +| 音视频入会 | `/v1/im/calls/rtc-join` 已存在。 | + +本地 `tinode` 库观察:`im_users` 有数据、`im_topics` 有群/会话、`im_subscriptions` 有订阅、`im_messages` 有大量单聊或通用消息、`im_group_messages` 有群消息、`im_feedback_submissions`、`im_abuse_reports`、`im_kafka_dead_letters` 等表已存在;`im_audit_logs` 本地为空,`im_devices` 和 `im_group_files` 数据不足,不能直接满足后台登录历史、设备历史和群文件验收。 + +已有内部查询资源: + +| 资源 | 已支持字段/场景 | +| --- | --- | +| `users` | 用户 ID、账号、手机号、状态、资料、凭证摘要、最后在线。 | +| `user_devices` | 用户设备列表。 | +| `groups` | Topic、群名、群主、群状态、治理开关、成员数。 | +| `groups/{topic}/members` | 群成员。 | +| `group_join-requests`、`group_invitations`、`group_announcements`、`group_mutes`、`group_admin-logs`、`group_files` | 群治理专题资源。 | +| `messages/{topic}/{seq}` | 单条消息详情。 | +| `files`、`files/{id}`、`files/{id}/links` | 文件列表、详情、链接关系。 | +| `feedback-submissions`、`abuse-reports`、`realname-verifications`、`audit-logs` | 用户反馈、举报、实名、审计。 | +| `dashboard/summary` | 当前只返回待办类指标,不足以覆盖鸽达达首页统计。 | + +已有 Kafka Admin Command: + +| 命令 | 用途 | +| --- | --- | +| `user.ban`、`user.unban` | 用户封禁/解封。 | +| `user.freeze`、`user.unfreeze` | 用户冻结/解冻。 | +| `user.device.kick`、`user.logout_all` | 设备踢下线、全端登出。 | +| `user.account.cancel`、`user.account.delete`、`user.password.reset` | 注销、删除、重置密码。 | +| `group.profile.update` | 群资料更新。 | +| `group.governance.update` | 群治理开关更新。 | +| `group.admin.set`、`group.admin.remove`、`group.owner.transfer` | 群角色管理。 | +| `group.member.mute`、`group.member.unmute`、`group.member.remove` | 群成员治理。 | +| `kafka.dead_letter.replay`、`ignore`、`resolve` | 死信处理。 | + +## 2. 改造原则 + +IM Server 只负责租户业务库的数据查询、业务状态变更、实时生效与审计事件产生,不直接依赖 HotGo 后台表。Admin Server 下发的配置、策略和治理命令必须具备幂等性,并通过事件结果回传 Admin。所有 internal query 必须保持只读、分页、最大 `per_page=200`、内部签名鉴权、字段脱敏。 + +响应协议继续沿用现有 `imHTTPResponse`:`code`、`data`、`reason`、`message`、`details`、`request_id`、`server_time`。写请求继续要求 `Idempotency-Key`,避免后台重复点击导致重复处置。 + +## 3. 支撑 20 个后台页面的 IM 改造 + +### 3.1 数据概览 + +现有 `dashboard/summary` 只覆盖死信、待处理 outbox、待实名、待举报、待反馈、活跃群、封禁用户等运营待办。需要新增统计字段以匹配鸽达达首页。 + +新增 internal query: + +| 路径 | 方法 | 说明 | +| --- | --- | --- | +| `/internal/admin-query/tenants/{tenant}/dashboard/overview` | GET | 返回总用户、活跃用户、总群组、总会话、总消息、今日消息。 | +| `/internal/admin-query/tenants/{tenant}/dashboard/trends` | GET | 返回最近 N 天用户增长与消息增长。 | + +字段口径: + +| 字段 | 计算建议 | +| --- | --- | +| `total_users` | `im_users` 有效用户数。 | +| `active_users` | `last_seen` 在指定窗口内或当天有消息/登录行为的用户数。 | +| `total_groups` | `im_group_settings.group_status='active'` 或有效群 topic 数。 | +| `total_conversations` | 有效 P2P topic + group topic 数。 | +| `total_messages` | `im_messages` + `im_group_messages`,按最终存储口径去重。 | +| `today_messages` | 当天创建的消息数,按租户时区对齐。 | +| `date`、`user_count`、`message_count` | 趋势表行字段。 | + +### 3.2 用户管理 + +现有 `users`、`users/{id}`、`users/{id}/devices` 基本可支撑。需要补足鸽达达页面要求的筛选和字段脱敏。 + +改造点: + +| 项 | 内容 | +| --- | --- | +| 查询筛选 | 增加 `phone`、`public_account`、`state`、`created_at_start/end`、`last_seen_start/end`。 | +| 字段补充 | 返回 `nickname`、头像文件 ID、性别、地区、签名、认证方式摘要、设备数、最近登录 IP。 | +| 状态映射 | 将 Tinode `state` 转换为 Admin 字典所需状态;保留原始值 `raw_state` 便于排错。 | +| 治理命令 | 继续使用已有 `user.ban/unban/freeze/unfreeze/device.kick/logout_all/account.cancel/account.delete/password.reset`。 | +| 审计 | 每次状态变更、重置密码、踢设备必须写 `im_audit_logs` 并产出 Kafka command result。 | + +枚举建议:`user_state` 包含正常、封禁、冻结、注销、删除;`device_platform` 包含 `ios`、`android`、`web`、`desktop`、`unknown`。 + +### 3.3 用户分组 + +当前 IM Server 没有看到独立的后台用户策略分组执行层。Admin 将新增策略分组后,IM Server 需要消费配置事件并在运行时执行。 + +新增能力: + +| 能力 | 说明 | +| --- | --- | +| 策略缓存 | 按租户缓存 `policy_group`、成员关系、优先级与规则。 | +| 匹配入口 | 登录、注册、加好友、建群、入群、发消息、上传文件、发起音视频前匹配策略。 | +| 配置事件 | 消费 `user_policy_group.updated`、`user_policy_group.deleted`,刷新缓存。 | +| 命中审计 | 命中 `deny`、`challenge` 或高风险规则时写审计与风控日志。 | + +策略动作枚举:`allow` 放行、`deny` 拦截、`challenge` 二次验证、`audit_only` 仅记录。 + +好友策略需要在后端覆盖所有移动端入口。禁止加好友时,IM Server 不能只让客户端隐藏按钮,而要在搜索用户、查看个人名片、群成员资料、二维码解析、好友申请、邀请注册自动加好友、推荐联系人等入口统一拦截或返回脱敏数据。策略字段建议包含 `allow_add_friend`、`direct_add_friend`、`max_friend_count`、`allow_qrcode_add_friend`、`allow_group_member_profile`、`hide_profile_when_friend_disabled`。 + +### 3.4 群组管理 + +已有 `groups` 查询与群治理命令可复用。需要补鸽达达群状态语义和可能的群封停动作。 + +改造点: + +| 项 | 内容 | +| --- | --- | +| 字段 | 确保 `topic`、`group_name`、`owner_id`、`group_status`、`business_member_count`、`join_approval`、`mute_all`、`created_at` 可稳定返回。 | +| 状态 | 明确 `group_status` 值:`active`、`muted`、`suspended`、`dissolved`。 | +| 命令 | 已有资料更新、治理设置、角色与成员治理;若需求要求群封停/解封,新增 `group.suspend`、`group.unsuspend`。 | +| 审计 | 群资料、治理开关、解散、禁言、成员操作均写 `im_group_admin_logs` 与 `im_audit_logs`。 | + +### 3.5 群成员管理 + +现有 `groups/{topic}/members` 与成员命令可支撑。需要确保成员列表返回群角色、禁言状态与入群时间。 + +字段建议:`topic`、`user_id`、`nickname`、`display_name`、`role`、`status`、`muted_until`、`joined_at`、`last_seen`。`role` 枚举为 `owner`、`admin`、`member`;`status` 枚举为 `active`、`muted`、`removed`。 + +按钮对应命令:设管理员 `group.admin.set`、取消管理员 `group.admin.remove`、转让群主 `group.owner.transfer`、成员禁言 `group.member.mute`、解除禁言 `group.member.unmute`、移除成员 `group.member.remove`。 + +### 3.6 邀请码管理 + +IM Server 注册链路已在 `user.go` 中记录邀请码使用,`RecordUse` 已存在。需要解决邀请码格式冲突:鸽达达需求为 6 位数字,既有 IM 文档曾要求 5 位 ASCII 字母。若严格复刻鸽达达,应将 IM 注册校验、错误提示、唯一性检查、大小写处理统一改为 6 位数字。 + +改造点: + +| 项 | 内容 | +| --- | --- | +| 校验 | 仅允许 `^[0-9]{6}$`,不做大小写转换。 | +| 使用记录 | 注册成功后写 `im_registration_invite_uses`,包含 `code`、`user_id`、`request_id`、`used_at`。 | +| 并发 | 使用事务或唯一约束防止超发。 | +| 状态 | `unused`、`used`、`expired`、`revoked`。 | + +### 3.7 单聊消息 + +当前只有单条消息 detail 查询,不足以支撑后台列表。需要新增 internal message list resource。 + +新增 internal query: + +| 路径 | 方法 | 查询参数 | +| --- | --- | --- | +| `/internal/admin-query/tenants/{tenant}/private-messages` | GET | `topic`、`from_user_id`、`to_user_id`、`content_type`、`status`、`keyword`、`created_at_start/end`、`page`、`per_page`。 | +| `/internal/admin-query/tenants/{tenant}/messages/{topic}/{seq}` | GET | 已有,继续作为详情。 | + +返回字段:`topic`、`seq_id`、`from_user_id`、`to_user_id`、`head`、`content_type`、`content_excerpt`、`status`、`created_at`、`updated_at`。全文内容只在详情返回,并根据 Admin 权限决定是否脱敏。 + +新增 Kafka 命令建议:`message.hide`、`message.unhide`、`message.delete`、`message.restore`。如果只允许软删除,命令语义必须明确为后台不可见还是客户端不可见。 + +### 3.8 群聊消息 + +新增 internal group message list resource。可读取 `im_group_messages` 或统一消息表,但需要确保与客户端可见状态一致。 + +路径:`/internal/admin-query/tenants/{tenant}/group-messages`。 + +筛选:`topic`、`sender_user_id`、`content_type`、`status`、`keyword`、`created_at_start/end`。返回字段在单聊基础上增加 `group_name`、`reply_to_seq`、`at_users`。 + +处置命令复用消息命令,审计动作记录为 `message.hidden`、`message.deleted`、`message.restored`。 + +### 3.9 敏感词库 + +IM Server 需要新增敏感词运行时,而词库主数据由 Admin 控制库维护。IM Server 只消费配置事件和执行匹配。 + +改造点: + +| 项 | 内容 | +| --- | --- | +| 缓存 | 按租户加载敏感词,支持分组、场景、优先级、状态。 | +| 匹配场景 | 消息发送、编辑消息、用户昵称/签名、群名/公告、反馈/举报内容。 | +| 动作 | `block` 拦截、`replace` 替换、`review` 进入审核、`audit_only` 仅审计。 | +| 审计 | 命中记录写 `im_sensitive_word_hits` 或统一 `im_audit_logs`。 | +| 性能 | 正常词库用 AC 自动机或等价高效结构;正则词单独限量执行。 | + +### 3.10 文件管理 + +已有 internal `files`、`files/{id}`、`files/{id}/links` 与文件生命周期 Kafka 基础。需要补后台预览、下载和处置一致性。 + +改造点: + +| 项 | 内容 | +| --- | --- | +| 文件状态 | `normal`、`quarantined`、`deleted`、`orphan`。 | +| 审核状态 | 复用 `pending`、`running`、`clean`、`risky`、`failed`。 | +| 预览下载 | 提供短期签名 URL 或代理下载,禁止返回永久存储地址。 | +| 隔离删除 | 后台命令应更新文件状态、断开消息/群文件链接、写审计。 | +| 安全审核 | 接入内容安全/恶意文件检测结果,并可重试审核。 | + +### 3.11 用户反馈 + +已有 `feedback-submissions` 资源可查询。需要补处理状态回写能力和状态审计。 + +新增能力: + +| 能力 | 说明 | +| --- | --- | +| 状态更新 | 提供 Kafka 命令或内部写接口:`feedback.status.update`。 | +| 附件 | 通过文件链接查询反馈附件,并返回签名预览 URL。 | +| 分类 | 分类来自 `im_feedback_config.categories`,Admin 配置后 IM bootstrap 同步。 | +| 审计 | 处理人、处理备注、前后状态写入审计。 | + +状态枚举:`received`、`processing`、`resolved`、`closed`。 + +### 3.12 操作日志 + +IM Server 需要补充业务侧审计完整性。当前本地 `im_audit_logs` 为空,不能支撑后台多页面审计。 + +写入场景:用户状态变更、群治理、消息处置、文件处置、反馈处理、敏感词命中、登录成功/失败、配置热更新、Kafka 命令执行结果。 + +字段:`request_id`、`actor_user_id`、`action`、`target_type`、`target_id`、`ip`、`user_agent`、`detail`、`created_at`。`detail` 中敏感值必须脱敏或存 hash。 + +### 3.13 用户账号审计 + +新增或扩展 internal resource `user-account-audit`,按用户聚合账号生命周期事件。 + +路径:`/internal/admin-query/tenants/{tenant}/user-account-audit`。 + +筛选:`user_id`、`action`、`operator_id`、`ip`、`created_at_start/end`。返回:`event_id`、`user_id`、`action`、`operator_type`、`operator_id`、`before_state`、`after_state`、`reason`、`ip`、`device_id`、`created_at`。 + +动作枚举:`registered`、`login_success`、`logout`、`login_failed`、`password_changed`、`password_reset`、`banned`、`unbanned`、`frozen`、`unfrozen`、`device_kicked`、`cancelled`、`deleted`。 + +### 3.14 群事件审计 + +已有 `im_group_admin_logs` 与 group outbox,可新增 internal resource `group-event-audit` 或复用 `group_admin-logs` 扩展为全局查询。 + +路径:`/internal/admin-query/tenants/{tenant}/group-event-audit`。 + +筛选:`topic`、`operator_id`、`target_user_id`、`action`、`created_at_start/end`。返回:`event_id`、`topic`、`operator_id`、`target_user_id`、`action`、`object_type`、`object_id`、`before_data`、`after_data`、`created_at`。 + +### 3.15 管理员登录历史 + +该页面主要由 Admin Server 的 HotGo 登录日志提供,IM Server 无需改造。若以后要展示 Admin 调用 IM internal API 的访问审计,可在 internal API 层增加 `internal_api_access_logs`,但不作为复刻 P0。 + +### 3.16 用户登录历史 + +当前缺少完整登录历史表与查询。需要在认证成功、token 刷新、主动/被动登出时记录日志。 + +新增表建议:`im_user_login_logs`。 + +字段:`id`、`user_id`、`login_type`、`device_id`、`platform`、`ip`、`region`、`user_agent`、`request_id`、`result`、`login_at`、`logout_at`、`created_at`。 + +新增 internal query:`/internal/admin-query/tenants/{tenant}/user-login-logs`,支持按 `user_id`、`phone/account`、`ip`、`platform`、`login_at_start/end` 查询。 + +### 3.17 IP 账号统计 + +新增聚合查询,来源为注册、登录成功、登录失败、风控命中日志。 + +路径:`/internal/admin-query/tenants/{tenant}/ip-account-stats`。 + +字段:`ip`、`region`、`user_count`、`register_count`、`login_count`、`failure_count`、`first_seen_at`、`last_seen_at`、`risk_level`。该接口可以实时聚合,也可以由定时任务写入快照表以降低查询成本。 + +### 3.18 登录失败日志 + +新增表建议:`im_login_failure_logs`。 + +字段:`id`、`account`、`user_id`、`failure_type`、`reason`、`ip`、`region`、`device_id`、`platform`、`user_agent`、`request_id`、`attempt_at`。 + +新增 internal query:`/internal/admin-query/tenants/{tenant}/login-failure-logs`。失败类型枚举:`password_error`、`code_error`、`account_not_found`、`account_disabled`、`rate_limited`、`risk_blocked`。 + +### 3.19 私域设置 + +IM Server 需要确保客户端 `/v1/im/bootstrap` 使用 Admin 发布后的配置,并支持热更新。 + +配置来源: + +| 配置 | 使用位置 | +| --- | --- | +| 注册/邀请码/短信 | 注册、验证码、密码找回。 | +| 上传/文件审核 | 上传、预览、消息附件。 | +| 反馈/邮箱/通知 | 客户端入口与提交表单。 | +| 版本/工作台 | bootstrap、app release、workbench。 | +| 声网/音视频 | `/v1/im/calls/rtc-join`。 | +| 好友功能 | `/v1/im/bootstrap` 返回 `global_friend_enabled`、入口隐藏建议和提示文案;实际拦截由后端完成。 | +| 消息宵禁 | `/v1/im/bootstrap` 返回当前宵禁摘要和提示文案;发送消息时仍以后端实时校验为准。 | + +改造点:配置事件必须带版本号;IM 缓存应支持按租户回滚到上一发布版本;bootstrap 返回的配置只包含客户端需要的非敏感字段。 + +### 3.20 安全策略 + +IM Server 是安全策略的执行层。需要将 Admin 发布的策略接入登录、验证码、消息、文件、群、好友、实名、音视频等关键入口。全局禁止加好友和宵禁必须后端强制执行,客户端展示仅作为体验优化。 + +策略项: + +| 策略 | 执行点 | +| --- | --- | +| 密码强度 | 注册、改密、重置密码。 | +| 登录限频 | 登录、验证码校验、密码找回。 | +| IP 黑白名单 | 登录、注册、发消息、上传文件。 | +| 敏感词 | 消息、资料、群资料、反馈。 | +| 文件审核 | 上传完成、消息发送前后、后台重审。 | +| 实名要求 | 加好友、建群、入群、发起音视频等敏感能力。 | +| 风控挑战 | 登录异常、IP 异常、设备异常。 | +| 全局好友功能 | 用户搜索、好友申请、二维码解析、群成员资料、个人名片、推荐联系人、邀请注册自动加好友。 | +| 消息宵禁 | 单聊/私聊发送消息入口,默认拦截 P2P topic 的 `{pub}`;如产品确认群聊也管控,可扩展到 group topic。 | + +动作枚举统一为 `allow`、`challenge`、`block`、`audit_only`。 + +全局禁止加好友执行规则: + +| 入口 | 后端行为 | +| --- | --- | +| 用户搜索 `/v1/im/users/search` | 可直接返回空列表,或返回脱敏结果但不允许发起好友申请。 | +| 用户详情 `/v1/im/users/{id}` | 非好友场景隐藏手机号、账号、二维码、个人扩展资料,只保留必要公开字段。 | +| 好友申请 `/v1/im/friend-requests` | 返回业务错误 reason,例如 `FRIEND_DISABLED`。 | +| 二维码解析 `/v1/im/qrcodes/resolve` | 禁止解析出可加好友目标,或返回脱敏提示。 | +| 群成员资料 | 群成员列表可保留昵称/头像用于群聊展示,但点击个人信息时返回脱敏资料并禁止添加。 | +| 邀请注册自动加好友 | 注册成功后跳过自动加好友并写审计,或按策略直接拒绝该邀请码能力。 | + +宵禁执行规则: + +| 项 | 说明 | +| --- | --- | +| 配置 | `message_curfew_enabled`、`message_curfew_rules`,规则包含时区、日期类型、开始时间、结束时间、scope、白名单、提示文案。 | +| 判断时机 | P2P topic 发布消息前校验;HTTP 消息发送、转发、草稿转发送等入口也要校验。 | +| 拦截结果 | 返回明确 reason,例如 `MESSAGE_CURFEW_ACTIVE`,并记录 `im_audit_logs` 或风控日志。 | +| 白名单 | 支持用户、系统账号、特定会话豁免。 | +| 跨天 | 支持如下班后 `18:00-09:00` 的跨天时间段。 | + +## 4. Internal Admin Query 新增清单 + +建议在 `server/internal_admin_query_resources.go` 增补资源,并在 `server/internal_api.go` 路由分发中注册: + +| 资源 | 必要性 | 说明 | +| --- | --- | --- | +| `dashboard/overview`、`dashboard/trends` | P0 | 首页统计。 | +| `private-messages`、`group-messages` | P0 | 消息管理列表。 | +| `user-login-logs`、`login-failure-logs` | P0 | 登录审计。 | +| `ip-account-stats` | P1 | IP 风险聚合。 | +| `user-account-audit`、`group-event-audit` | P1 | 专题审计。 | +| `sensitive-word-hits` | P1 | 敏感词命中查询。 | + +所有列表接口返回现有结构:`items`、`total`、`page`、`per_page`。时间筛选继续使用 `created_at_start`、`created_at_end` 等字段,UID 参数继续支持数字 ID 与 `usr...` 编码。 + +## 5. Kafka 命令新增清单 + +已有用户与群治理命令保留。为复刻后台补充以下命令: + +| 命令 | 说明 | +| --- | --- | +| `message.hide` | 后台隐藏消息,客户端不可见或后台标记隐藏。 | +| `message.unhide` | 恢复隐藏消息。 | +| `message.delete` | 后台删除消息,建议软删除。 | +| `message.restore` | 恢复误删消息。 | +| `file.quarantine` | 文件隔离。 | +| `file.delete` | 文件删除。 | +| `feedback.status.update` | 反馈处理状态更新。 | +| `group.suspend`、`group.unsuspend` | 如产品确认需要群封停/解封,则新增。 | +| `security.policy.reload` | 强制刷新安全策略缓存。 | +| `friend.policy.reload` | 强制刷新好友入口策略缓存,可与 `security.policy.reload` 合并实现。 | +| `curfew.policy.reload` | 强制刷新宵禁策略缓存,可与 `security.policy.reload` 合并实现。 | + +命令 payload 必须包含 `command_id`、`actor_user_id`、`reason`、目标 ID、`version` 或 `expected_version`。执行后发布 result,状态为 `success`、`failed`、`dead`。 + +## 6. 数据库改造建议 + +配套建表与兼容迁移脚本见 [`sql/IM-Server改造.sql`](sql/IM-Server改造.sql)。脚本默认使用本地 `tinode`,上线时必须对每个独立租户数据库分别执行,租户业务表不增加 `tenant_id`。 + +租户库新增或补强表: + +| 表 | 用途 | +| --- | --- | +| `im_user_login_logs` | 用户登录成功、登出、刷新等历史。 | +| `im_login_failure_logs` | 登录失败与风控拦截历史。 | +| `im_message_admin_actions` | 后台消息处置记录,可与消息表状态解耦。 | +| `im_sensitive_word_hits` | 敏感词命中记录。 | +| `im_ip_account_stats_daily` | IP 账号统计快照,可选。 | + +已有表需补数据写入:`im_audit_logs` 当前本地为空,需要在关键业务入口统一写;`im_devices` 本地为空,需要确认设备绑定/解绑路径是否完整;`im_group_files` 本地为空,需要验证群文件写入链路。 + +索引建议:消息列表按 `(topic, created_at)`、`(from, created_at)`;登录日志按 `(user_id, login_at)`、`(ip, login_at)`;失败日志按 `(account, attempt_at)`、`(ip, attempt_at)`;敏感词命中按 `(scene, created_at)`、`(user_id, created_at)`。 + +## 7. 验收要求 + +1. Admin 20 个页面的查询按钮均能通过 internal query 返回真实数据或标准空态。 +2. 用户、群、消息、文件、反馈的后台写操作均通过 Kafka 命令,具备幂等、结果回传、死信处理。 +3. 登录历史、失败日志、账号审计、群事件审计不再依赖空表,关键入口可产生真实审计数据。 +4. 敏感词与安全策略变更可热更新,无需重启 IM Server。 +5. 所有 internal query 均保持只读、分页、鉴权、脱敏和错误 reason 规范。 +6. 邀请码格式在 Admin 与 IM 注册链路完全一致,避免后台生成后客户端无法使用。 diff --git a/docs/admin-adfahi-research/api-samples.json b/docs/admin-adfahi-research/api-samples.json new file mode 100644 index 0000000..6cab855 --- /dev/null +++ b/docs/admin-adfahi-research/api-samples.json @@ -0,0 +1,70 @@ +[ + { + "endpoint": "/admin/stats", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/users", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/user-policy-groups", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/groups", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/groups/events", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/invite-codes", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/messages/search", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/sensitive-words", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/feedbacks", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/audit/logs", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/audit/user-account-logs", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/admin-login-history", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/user-login-history", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/audit/ip-user-stats", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/login-failure-logs", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/tenant-settings", + "error": "TypeError: window.fetch is not a function" + }, + { + "endpoint": "/admin/security-settings", + "error": "TypeError: window.fetch is not a function" + } +] \ No newline at end of file diff --git a/docs/admin-adfahi-research/index-RosMaKu2.js b/docs/admin-adfahi-research/index-RosMaKu2.js new file mode 100644 index 0000000..b10b489 --- /dev/null +++ b/docs/admin-adfahi-research/index-RosMaKu2.js @@ -0,0 +1,158 @@ +var Dh=Object.defineProperty;var Ph=(e,t,r)=>t in e?Dh(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var Or=(e,t,r)=>Ph(e,typeof t!="symbol"?t+"":t,r);import{r as g,c as Vn,g as yu,e as Nh,R as Gt,u as wu,f as Lh,h as Mh,i as Bh,j as Ge,N as la,B as Uh}from"./react-vendor-XHtC9wm2.js";import{r as zh,L as Ko,T as Cr,F as b,R as $h,a as sc,b as po,c as $n,d as Go,e as Wh,f as Hh,g as Vh,h as ac,i as ca,j as Ks,k as xi,l as Kh,m as ci,n as Su,o as Tu,p as bu,q as ic,s as Eu,t as Gh,u as Yh,v as oc,w as qh,x as lc,y as Xh,z as Yo,A as cc,M as Jh,S as Ie,B as da,D as Zh,C as zi,E as Me,I as ve,G as Mn,H as Y,Q as Qh,J as je,K as pi,N as em,O as E,P as be,U as $e,V as Es,W as Ct,X as tn,Y as Ye,Z as tm,_ as ju,$ as Iu,a0 as $r,a1 as $i,a2 as Mr,a3 as Te,a4 as un,a5 as at,a6 as qo,a7 as Ar,a8 as Xt,a9 as xl,aa as Gs,ab as pl,ac as rr,ad as Cu,ae as rn,af as Xs,ag as Cn,ah as zn,ai as An,aj as gi,ak as _i,al as vs,am as Au,an as jr,ao as go,ap as dc,aq as gl,ar as rt,as as ua,at as Js,au as js,av as rm,aw as nm,ax as Zs,ay as sm,az as Xo,aA as uc,aB as am,aC as _o,aD as im,aE as om,aF as lm}from"./antd-vendor-CXEPnBxj.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))n(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function r(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?i.credentials="include":s.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(s){if(s.ep)return;s.ep=!0;const i=r(s);fetch(s.href,i)}})();var ku={exports:{}},Wi={};/** + * @license React + * react-jsx-runtime.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var cm=g,dm=Symbol.for("react.element"),um=Symbol.for("react.fragment"),fm=Object.prototype.hasOwnProperty,hm=cm.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,mm={key:!0,ref:!0,__self:!0,__source:!0};function Fu(e,t,r){var n,s={},i=null,o=null;r!==void 0&&(i=""+r),t.key!==void 0&&(i=""+t.key),t.ref!==void 0&&(o=t.ref);for(n in t)fm.call(t,n)&&!mm.hasOwnProperty(n)&&(s[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps,t)s[n]===void 0&&(s[n]=t[n]);return{$$typeof:dm,type:e,key:i,ref:o,props:s,_owner:hm.current}}Wi.Fragment=um;Wi.jsx=Fu;Wi.jsxs=Fu;ku.exports=Wi;var a=ku.exports,Jo={},fc=Vn;Jo.createRoot=fc.createRoot,Jo.hydrateRoot=fc.hydrateRoot;var Hi={},Ou={exports:{}};(function(e){function t(r){return r&&r.__esModule?r:{default:r}}e.exports=t,e.exports.__esModule=!0,e.exports.default=e.exports})(Ou);var Vi=Ou.exports,Ki={};Object.defineProperty(Ki,"__esModule",{value:!0});Ki.default=void 0;var xm={items_per_page:"条/页",jump_to:"跳至",jump_to_confirm:"确定",page:"页",prev_page:"上一页",next_page:"下一页",prev_5:"向前 5 页",next_5:"向后 5 页",prev_3:"向前 3 页",next_3:"向后 3 页",page_size:"页码"};Ki.default=xm;var Gi={},ba={},Yi={},Ru={exports:{}},Du={exports:{}},Pu={exports:{}},Nu={exports:{}};(function(e){function t(r){"@babel/helpers - typeof";return e.exports=t=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(n){return typeof n}:function(n){return n&&typeof Symbol=="function"&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n},e.exports.__esModule=!0,e.exports.default=e.exports,t(r)}e.exports=t,e.exports.__esModule=!0,e.exports.default=e.exports})(Nu);var Lu=Nu.exports,Mu={exports:{}};(function(e){var t=Lu.default;function r(n,s){if(t(n)!="object"||!n)return n;var i=n[Symbol.toPrimitive];if(i!==void 0){var o=i.call(n,s||"default");if(t(o)!="object")return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return(s==="string"?String:Number)(n)}e.exports=r,e.exports.__esModule=!0,e.exports.default=e.exports})(Mu);var pm=Mu.exports;(function(e){var t=Lu.default,r=pm;function n(s){var i=r(s,"string");return t(i)=="symbol"?i:i+""}e.exports=n,e.exports.__esModule=!0,e.exports.default=e.exports})(Pu);var gm=Pu.exports;(function(e){var t=gm;function r(n,s,i){return(s=t(s))in n?Object.defineProperty(n,s,{value:i,enumerable:!0,configurable:!0,writable:!0}):n[s]=i,n}e.exports=r,e.exports.__esModule=!0,e.exports.default=e.exports})(Du);var _m=Du.exports;(function(e){var t=_m;function r(s,i){var o=Object.keys(s);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(s);i&&(l=l.filter(function(c){return Object.getOwnPropertyDescriptor(s,c).enumerable})),o.push.apply(o,l)}return o}function n(s){for(var i=1;i{let t;const r=new Set,n=(u,h)=>{const m=typeof u=="function"?u(t):u;if(!Object.is(m,t)){const _=t;t=h??(typeof m!="object"||m===null)?m:Object.assign({},t,m),r.forEach(x=>x(t,_))}},s=()=>t,c={setState:n,getState:s,getInitialState:()=>d,subscribe:u=>(r.add(u),()=>r.delete(u)),destroy:()=>{(Nm?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),r.clear()}},d=t=e(n,s,c);return c},Lm=e=>e?mc(e):mc;var zu={exports:{}},$u={},Wu={exports:{}},Hu={};/** + * @license React + * use-sync-external-store-shim.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Is=g;function Mm(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var Bm=typeof Object.is=="function"?Object.is:Mm,Um=Is.useState,zm=Is.useEffect,$m=Is.useLayoutEffect,Wm=Is.useDebugValue;function Hm(e,t){var r=t(),n=Um({inst:{value:r,getSnapshot:t}}),s=n[0].inst,i=n[1];return $m(function(){s.value=r,s.getSnapshot=t,vo(s)&&i({inst:s})},[e,r,t]),zm(function(){return vo(s)&&i({inst:s}),e(function(){vo(s)&&i({inst:s})})},[e]),Wm(r),r}function vo(e){var t=e.getSnapshot;e=e.value;try{var r=t();return!Bm(e,r)}catch{return!0}}function Vm(e,t){return t()}var Km=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?Vm:Hm;Hu.useSyncExternalStore=Is.useSyncExternalStore!==void 0?Is.useSyncExternalStore:Km;Wu.exports=Hu;var Gm=Wu.exports;/** + * @license React + * use-sync-external-store-shim/with-selector.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ji=g,Ym=Gm;function qm(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var Xm=typeof Object.is=="function"?Object.is:qm,Jm=Ym.useSyncExternalStore,Zm=Ji.useRef,Qm=Ji.useEffect,ex=Ji.useMemo,tx=Ji.useDebugValue;$u.useSyncExternalStoreWithSelector=function(e,t,r,n,s){var i=Zm(null);if(i.current===null){var o={hasValue:!1,value:null};i.current=o}else o=i.current;i=ex(function(){function c(_){if(!d){if(d=!0,u=_,_=n(_),s!==void 0&&o.hasValue){var x=o.value;if(s(x,_))return h=x}return h=_}if(x=h,Xm(u,_))return x;var f=n(_);return s!==void 0&&s(x,f)?(u=_,x):(u=_,h=f)}var d=!1,u,h,m=r===void 0?null:r;return[function(){return c(t())},m===null?void 0:function(){return c(m())}]},[t,r,n,s]);var l=Jm(e,i[0],i[1]);return Qm(function(){o.hasValue=!0,o.value=l},[l]),tx(l),l};zu.exports=$u;var rx=zu.exports;const nx=yu(rx),Vu={},{useDebugValue:sx}=Gt,{useSyncExternalStoreWithSelector:ax}=nx;let xc=!1;const ix=e=>e;function ox(e,t=ix,r){(Vu?"production":void 0)!=="production"&&r&&!xc&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),xc=!0);const n=ax(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,r);return sx(n),n}const pc=e=>{(Vu?"production":void 0)!=="production"&&typeof e!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const t=typeof e=="function"?Lm(e):e,r=(n,s)=>ox(t,n,s);return Object.assign(r,t),r},_l=e=>e?pc(e):pc,lx={};function Ku(e,t){let r;try{r=e()}catch{return}return{getItem:s=>{var i;const o=c=>c===null?null:JSON.parse(c,void 0),l=(i=r.getItem(s))!=null?i:null;return l instanceof Promise?l.then(o):o(l)},setItem:(s,i)=>r.setItem(s,JSON.stringify(i,void 0)),removeItem:s=>r.removeItem(s)}}const fa=e=>t=>{try{const r=e(t);return r instanceof Promise?r:{then(n){return fa(n)(r)},catch(n){return this}}}catch(r){return{then(n){return this},catch(n){return fa(n)(r)}}}},cx=(e,t)=>(r,n,s)=>{let i={getStorage:()=>localStorage,serialize:JSON.stringify,deserialize:JSON.parse,partialize:p=>p,version:0,merge:(p,I)=>({...I,...p}),...t},o=!1;const l=new Set,c=new Set;let d;try{d=i.getStorage()}catch{}if(!d)return e((...p)=>{console.warn(`[zustand persist middleware] Unable to update item '${i.name}', the given storage is currently unavailable.`),r(...p)},n,s);const u=fa(i.serialize),h=()=>{const p=i.partialize({...n()});let I;const S=u({state:p,version:i.version}).then(w=>d.setItem(i.name,w)).catch(w=>{I=w});if(I)throw I;return S},m=s.setState;s.setState=(p,I)=>{m(p,I),h()};const _=e((...p)=>{r(...p),h()},n,s);let x;const f=()=>{var p;if(!d)return;o=!1,l.forEach(S=>S(n()));const I=((p=i.onRehydrateStorage)==null?void 0:p.call(i,n()))||void 0;return fa(d.getItem.bind(d))(i.name).then(S=>{if(S)return i.deserialize(S)}).then(S=>{if(S)if(typeof S.version=="number"&&S.version!==i.version){if(i.migrate)return i.migrate(S.state,S.version);console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}else return S.state}).then(S=>{var w;return x=i.merge(S,(w=n())!=null?w:_),r(x,!0),h()}).then(()=>{I==null||I(x,void 0),o=!0,c.forEach(S=>S(x))}).catch(S=>{I==null||I(void 0,S)})};return s.persist={setOptions:p=>{i={...i,...p},p.getStorage&&(d=p.getStorage())},clearStorage:()=>{d==null||d.removeItem(i.name)},getOptions:()=>i,rehydrate:()=>f(),hasHydrated:()=>o,onHydrate:p=>(l.add(p),()=>{l.delete(p)}),onFinishHydration:p=>(c.add(p),()=>{c.delete(p)})},f(),x||_},dx=(e,t)=>(r,n,s)=>{let i={storage:Ku(()=>localStorage),partialize:f=>f,version:0,merge:(f,p)=>({...p,...f}),...t},o=!1;const l=new Set,c=new Set;let d=i.storage;if(!d)return e((...f)=>{console.warn(`[zustand persist middleware] Unable to update item '${i.name}', the given storage is currently unavailable.`),r(...f)},n,s);const u=()=>{const f=i.partialize({...n()});return d.setItem(i.name,{state:f,version:i.version})},h=s.setState;s.setState=(f,p)=>{h(f,p),u()};const m=e((...f)=>{r(...f),u()},n,s);s.getInitialState=()=>m;let _;const x=()=>{var f,p;if(!d)return;o=!1,l.forEach(S=>{var w;return S((w=n())!=null?w:m)});const I=((p=i.onRehydrateStorage)==null?void 0:p.call(i,(f=n())!=null?f:m))||void 0;return fa(d.getItem.bind(d))(i.name).then(S=>{if(S)if(typeof S.version=="number"&&S.version!==i.version){if(i.migrate)return[!0,i.migrate(S.state,S.version)];console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}else return[!1,S.state];return[!1,void 0]}).then(S=>{var w;const[v,y]=S;if(_=i.merge(y,(w=n())!=null?w:m),r(_,!0),v)return u()}).then(()=>{I==null||I(_,void 0),_=n(),o=!0,c.forEach(S=>S(_))}).catch(S=>{I==null||I(void 0,S)})};return s.persist={setOptions:f=>{i={...i,...f},f.storage&&(d=f.storage)},clearStorage:()=>{d==null||d.removeItem(i.name)},getOptions:()=>i,rehydrate:()=>x(),hasHydrated:()=>o,onHydrate:f=>(l.add(f),()=>{l.delete(f)}),onFinishHydration:f=>(c.add(f),()=>{c.delete(f)})},i.skipHydration||x(),_||m},ux=(e,t)=>"getStorage"in t||"serialize"in t||"deserialize"in t?((lx?"production":void 0)!=="production"&&console.warn("[DEPRECATED] `getStorage`, `serialize` and `deserialize` options are deprecated. Use `storage` option instead."),cx(e,t)):dx(e,t),fx=ux,gc="admin_access_token",pt=_l()(fx(e=>({admin:null,permissions:null,token:null,isAuthenticated:!1,setAuth:(t,r,n)=>{sessionStorage.setItem(gc,r),e({admin:t,token:r,permissions:n??t.permissions,isAuthenticated:!0})},setAdmin:t=>e({admin:t,permissions:t.permissions}),clearAuth:()=>{sessionStorage.removeItem(gc),e({admin:null,permissions:null,token:null,isAuthenticated:!1})}}),{name:"admin-auth-storage",storage:Ku(()=>sessionStorage),partialize:e=>({admin:e.admin,permissions:e.permissions,token:e.token,isAuthenticated:e.isAuthenticated})}));function hx(){for(var e=arguments.length,t=new Array(e),r=0;rn=>{t.forEach(s=>s(n))},t)}const Zi=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Os(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function vl(e){return"nodeType"in e}function _r(e){var t,r;return e?Os(e)?e:vl(e)&&(t=(r=e.ownerDocument)==null?void 0:r.defaultView)!=null?t:window:window}function yl(e){const{Document:t}=_r(e);return e instanceof t}function ja(e){return Os(e)?!1:e instanceof _r(e).HTMLElement}function Gu(e){return e instanceof _r(e).SVGElement}function Rs(e){return e?Os(e)?e.document:vl(e)?yl(e)?e:ja(e)||Gu(e)?e.ownerDocument:document:document:document}const sn=Zi?g.useLayoutEffect:g.useEffect;function wl(e){const t=g.useRef(e);return sn(()=>{t.current=e}),g.useCallback(function(){for(var r=arguments.length,n=new Array(r),s=0;s{e.current=setInterval(n,s)},[]),r=g.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,r]}function ha(e,t){t===void 0&&(t=[e]);const r=g.useRef(e);return sn(()=>{r.current!==e&&(r.current=e)},t),r}function Ia(e,t){const r=g.useRef();return g.useMemo(()=>{const n=e(r.current);return r.current=n,n},[...t])}function vi(e){const t=wl(e),r=g.useRef(null),n=g.useCallback(s=>{s!==r.current&&(t==null||t(s,r.current)),r.current=s},[]);return[r,n]}function Zo(e){const t=g.useRef();return g.useEffect(()=>{t.current=e},[e]),t.current}let yo={};function Ca(e,t){return g.useMemo(()=>{if(t)return t;const r=yo[e]==null?0:yo[e]+1;return yo[e]=r,e+"-"+r},[e,t])}function Yu(e){return function(t){for(var r=arguments.length,n=new Array(r>1?r-1:0),s=1;s{const l=Object.entries(o);for(const[c,d]of l){const u=i[c];u!=null&&(i[c]=u+e*d)}return i},{...t})}}const ys=Yu(1),yi=Yu(-1);function xx(e){return"clientX"in e&&"clientY"in e}function Sl(e){if(!e)return!1;const{KeyboardEvent:t}=_r(e.target);return t&&e instanceof t}function px(e){if(!e)return!1;const{TouchEvent:t}=_r(e.target);return t&&e instanceof t}function Qo(e){if(px(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:r}=e.touches[0];return{x:t,y:r}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:r}=e.changedTouches[0];return{x:t,y:r}}}return xx(e)?{x:e.clientX,y:e.clientY}:null}const ma=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:r}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(r?Math.round(r):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:r}=e;return"scaleX("+t+") scaleY("+r+")"}},Transform:{toString(e){if(e)return[ma.Translate.toString(e),ma.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:r,easing:n}=e;return t+" "+r+"ms "+n}}}),_c="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function gx(e){return e.matches(_c)?e:e.querySelector(_c)}const _x={display:"none"};function vx(e){let{id:t,value:r}=e;return Gt.createElement("div",{id:t,style:_x},r)}function yx(e){let{id:t,announcement:r,ariaLiveType:n="assertive"}=e;const s={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return Gt.createElement("div",{id:t,style:s,role:"status","aria-live":n,"aria-atomic":!0},r)}function wx(){const[e,t]=g.useState("");return{announce:g.useCallback(n=>{n!=null&&t(n)},[]),announcement:e}}const qu=g.createContext(null);function Sx(e){const t=g.useContext(qu);g.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of ");return t(e)},[e,t])}function Tx(){const[e]=g.useState(()=>new Set),t=g.useCallback(n=>(e.add(n),()=>e.delete(n)),[e]);return[g.useCallback(n=>{let{type:s,event:i}=n;e.forEach(o=>{var l;return(l=o[s])==null?void 0:l.call(o,i)})},[e]),t]}const bx={draggable:` + To pick up a draggable item, press the space bar. + While dragging, use the arrow keys to move the item. + Press space again to drop the item in its new position, or press escape to cancel. + `},Ex={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:r}=e;return r?"Draggable item "+t.id+" was moved over droppable area "+r.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:r}=e;return r?"Draggable item "+t.id+" was dropped over droppable area "+r.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function jx(e){let{announcements:t=Ex,container:r,hiddenTextDescribedById:n,screenReaderInstructions:s=bx}=e;const{announce:i,announcement:o}=wx(),l=Ca("DndLiveRegion"),[c,d]=g.useState(!1);if(g.useEffect(()=>{d(!0)},[]),Sx(g.useMemo(()=>({onDragStart(h){let{active:m}=h;i(t.onDragStart({active:m}))},onDragMove(h){let{active:m,over:_}=h;t.onDragMove&&i(t.onDragMove({active:m,over:_}))},onDragOver(h){let{active:m,over:_}=h;i(t.onDragOver({active:m,over:_}))},onDragEnd(h){let{active:m,over:_}=h;i(t.onDragEnd({active:m,over:_}))},onDragCancel(h){let{active:m,over:_}=h;i(t.onDragCancel({active:m,over:_}))}}),[i,t])),!c)return null;const u=Gt.createElement(Gt.Fragment,null,Gt.createElement(vx,{id:n,value:s.draggable}),Gt.createElement(yx,{id:l,announcement:o}));return r?Vn.createPortal(u,r):u}var Mt;(function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"})(Mt||(Mt={}));function wi(){}function Ix(e,t){return g.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function Cx(){for(var e=arguments.length,t=new Array(e),r=0;r[...t].filter(n=>n!=null),[...t])}const Kr=Object.freeze({x:0,y:0});function Ax(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function kx(e,t){let{data:{value:r}}=e,{data:{value:n}}=t;return r-n}function Fx(e,t){let{data:{value:r}}=e,{data:{value:n}}=t;return n-r}function Ox(e,t){if(!e||e.length===0)return null;const[r]=e;return r[t]}function vc(e,t,r){return t===void 0&&(t=e.left),r===void 0&&(r=e.top),{x:t+e.width*.5,y:r+e.height*.5}}const Rx=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const s=vc(t,t.left,t.top),i=[];for(const o of n){const{id:l}=o,c=r.get(l);if(c){const d=Ax(vc(c),s);i.push({id:l,data:{droppableContainer:o,value:d}})}}return i.sort(kx)};function Dx(e,t){const r=Math.max(t.top,e.top),n=Math.max(t.left,e.left),s=Math.min(t.left+t.width,e.left+e.width),i=Math.min(t.top+t.height,e.top+e.height),o=s-n,l=i-r;if(n{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const s=[];for(const i of n){const{id:o}=i,l=r.get(o);if(l){const c=Dx(l,t);c>0&&s.push({id:o,data:{droppableContainer:i,value:c}})}}return s.sort(Fx)};function Nx(e,t,r){return{...e,scaleX:t&&r?t.width/r.width:1,scaleY:t&&r?t.height/r.height:1}}function Xu(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Kr}function Lx(e){return function(r){for(var n=arguments.length,s=new Array(n>1?n-1:0),i=1;i({...o,top:o.top+e*l.y,bottom:o.bottom+e*l.y,left:o.left+e*l.x,right:o.right+e*l.x}),{...r})}}const Mx=Lx(1);function Bx(e){if(e.startsWith("matrix3d(")){const t=e.slice(9,-1).split(/, /);return{x:+t[12],y:+t[13],scaleX:+t[0],scaleY:+t[5]}}else if(e.startsWith("matrix(")){const t=e.slice(7,-1).split(/, /);return{x:+t[4],y:+t[5],scaleX:+t[0],scaleY:+t[3]}}return null}function Ux(e,t,r){const n=Bx(t);if(!n)return e;const{scaleX:s,scaleY:i,x:o,y:l}=n,c=e.left-o-(1-s)*parseFloat(r),d=e.top-l-(1-i)*parseFloat(r.slice(r.indexOf(" ")+1)),u=s?e.width/s:e.width,h=i?e.height/i:e.height;return{width:u,height:h,top:d,right:c+u,bottom:d+h,left:c}}const zx={ignoreTransform:!1};function Ds(e,t){t===void 0&&(t=zx);let r=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:d,transformOrigin:u}=_r(e).getComputedStyle(e);d&&(r=Ux(r,d,u))}const{top:n,left:s,width:i,height:o,bottom:l,right:c}=r;return{top:n,left:s,width:i,height:o,bottom:l,right:c}}function yc(e){return Ds(e,{ignoreTransform:!0})}function $x(e){const t=e.innerWidth,r=e.innerHeight;return{top:0,left:0,right:t,bottom:r,width:t,height:r}}function Wx(e,t){return t===void 0&&(t=_r(e).getComputedStyle(e)),t.position==="fixed"}function Hx(e,t){t===void 0&&(t=_r(e).getComputedStyle(e));const r=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(s=>{const i=t[s];return typeof i=="string"?r.test(i):!1})}function Tl(e,t){const r=[];function n(s){if(t!=null&&r.length>=t||!s)return r;if(yl(s)&&s.scrollingElement!=null&&!r.includes(s.scrollingElement))return r.push(s.scrollingElement),r;if(!ja(s)||Gu(s)||r.includes(s))return r;const i=_r(e).getComputedStyle(s);return s!==e&&Hx(s,i)&&r.push(s),Wx(s,i)?r:n(s.parentNode)}return e?n(e):r}function Ju(e){const[t]=Tl(e,1);return t??null}function wo(e){return!Zi||!e?null:Os(e)?e:vl(e)?yl(e)||e===Rs(e).scrollingElement?window:ja(e)?e:null:null}function Zu(e){return Os(e)?e.scrollX:e.scrollLeft}function Qu(e){return Os(e)?e.scrollY:e.scrollTop}function el(e){return{x:Zu(e),y:Qu(e)}}var Bt;(function(e){e[e.Forward=1]="Forward",e[e.Backward=-1]="Backward"})(Bt||(Bt={}));function e0(e){return!Zi||!e?!1:e===document.scrollingElement}function t0(e){const t={x:0,y:0},r=e0(e)?{height:window.innerHeight,width:window.innerWidth}:{height:e.clientHeight,width:e.clientWidth},n={x:e.scrollWidth-r.width,y:e.scrollHeight-r.height},s=e.scrollTop<=t.y,i=e.scrollLeft<=t.x,o=e.scrollTop>=n.y,l=e.scrollLeft>=n.x;return{isTop:s,isLeft:i,isBottom:o,isRight:l,maxScroll:n,minScroll:t}}const Vx={x:.2,y:.2};function Kx(e,t,r,n,s){let{top:i,left:o,right:l,bottom:c}=r;n===void 0&&(n=10),s===void 0&&(s=Vx);const{isTop:d,isBottom:u,isLeft:h,isRight:m}=t0(e),_={x:0,y:0},x={x:0,y:0},f={height:t.height*s.y,width:t.width*s.x};return!d&&i<=t.top+f.height?(_.y=Bt.Backward,x.y=n*Math.abs((t.top+f.height-i)/f.height)):!u&&c>=t.bottom-f.height&&(_.y=Bt.Forward,x.y=n*Math.abs((t.bottom-f.height-c)/f.height)),!m&&l>=t.right-f.width?(_.x=Bt.Forward,x.x=n*Math.abs((t.right-f.width-l)/f.width)):!h&&o<=t.left+f.width&&(_.x=Bt.Backward,x.x=n*Math.abs((t.left+f.width-o)/f.width)),{direction:_,speed:x}}function Gx(e){if(e===document.scrollingElement){const{innerWidth:i,innerHeight:o}=window;return{top:0,left:0,right:i,bottom:o,width:i,height:o}}const{top:t,left:r,right:n,bottom:s}=e.getBoundingClientRect();return{top:t,left:r,right:n,bottom:s,width:e.clientWidth,height:e.clientHeight}}function r0(e){return e.reduce((t,r)=>ys(t,el(r)),Kr)}function Yx(e){return e.reduce((t,r)=>t+Zu(r),0)}function qx(e){return e.reduce((t,r)=>t+Qu(r),0)}function Xx(e,t){if(t===void 0&&(t=Ds),!e)return;const{top:r,left:n,bottom:s,right:i}=t(e);Ju(e)&&(s<=0||i<=0||r>=window.innerHeight||n>=window.innerWidth)&&e.scrollIntoView({block:"center",inline:"center"})}const Jx=[["x",["left","right"],Yx],["y",["top","bottom"],qx]];class bl{constructor(t,r){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const n=Tl(r),s=r0(n);this.rect={...t},this.width=t.width,this.height=t.height;for(const[i,o,l]of Jx)for(const c of o)Object.defineProperty(this,c,{get:()=>{const d=l(n),u=s[i]-d;return this.rect[c]+u},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class Qs{constructor(t){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(r=>{var n;return(n=this.target)==null?void 0:n.removeEventListener(...r)})},this.target=t}add(t,r,n){var s;(s=this.target)==null||s.addEventListener(t,r,n),this.listeners.push([t,r,n])}}function Zx(e){const{EventTarget:t}=_r(e);return e instanceof t?e:Rs(e)}function So(e,t){const r=Math.abs(e.x),n=Math.abs(e.y);return typeof t=="number"?Math.sqrt(r**2+n**2)>t:"x"in t&&"y"in t?r>t.x&&n>t.y:"x"in t?r>t.x:"y"in t?n>t.y:!1}var Lr;(function(e){e.Click="click",e.DragStart="dragstart",e.Keydown="keydown",e.ContextMenu="contextmenu",e.Resize="resize",e.SelectionChange="selectionchange",e.VisibilityChange="visibilitychange"})(Lr||(Lr={}));function wc(e){e.preventDefault()}function Qx(e){e.stopPropagation()}var lt;(function(e){e.Space="Space",e.Down="ArrowDown",e.Right="ArrowRight",e.Left="ArrowLeft",e.Up="ArrowUp",e.Esc="Escape",e.Enter="Enter",e.Tab="Tab"})(lt||(lt={}));const n0={start:[lt.Space,lt.Enter],cancel:[lt.Esc],end:[lt.Space,lt.Enter,lt.Tab]},ep=(e,t)=>{let{currentCoordinates:r}=t;switch(e.code){case lt.Right:return{...r,x:r.x+25};case lt.Left:return{...r,x:r.x-25};case lt.Down:return{...r,y:r.y+25};case lt.Up:return{...r,y:r.y-25}}};class s0{constructor(t){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=t;const{event:{target:r}}=t;this.props=t,this.listeners=new Qs(Rs(r)),this.windowListeners=new Qs(_r(r)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(Lr.Resize,this.handleCancel),this.windowListeners.add(Lr.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(Lr.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:t,onStart:r}=this.props,n=t.node.current;n&&Xx(n),r(Kr)}handleKeyDown(t){if(Sl(t)){const{active:r,context:n,options:s}=this.props,{keyboardCodes:i=n0,coordinateGetter:o=ep,scrollBehavior:l="smooth"}=s,{code:c}=t;if(i.end.includes(c)){this.handleEnd(t);return}if(i.cancel.includes(c)){this.handleCancel(t);return}const{collisionRect:d}=n.current,u=d?{x:d.left,y:d.top}:Kr;this.referenceCoordinates||(this.referenceCoordinates=u);const h=o(t,{active:r,context:n.current,currentCoordinates:u});if(h){const m=yi(h,u),_={x:0,y:0},{scrollableAncestors:x}=n.current;for(const f of x){const p=t.code,{isTop:I,isRight:S,isLeft:w,isBottom:v,maxScroll:y,minScroll:R}=t0(f),j=Gx(f),T={x:Math.min(p===lt.Right?j.right-j.width/2:j.right,Math.max(p===lt.Right?j.left:j.left+j.width/2,h.x)),y:Math.min(p===lt.Down?j.bottom-j.height/2:j.bottom,Math.max(p===lt.Down?j.top:j.top+j.height/2,h.y))},C=p===lt.Right&&!S||p===lt.Left&&!w,F=p===lt.Down&&!v||p===lt.Up&&!I;if(C&&T.x!==h.x){const P=f.scrollLeft+m.x,V=p===lt.Right&&P<=y.x||p===lt.Left&&P>=R.x;if(V&&!m.y){f.scrollTo({left:P,behavior:l});return}V?_.x=f.scrollLeft-P:_.x=p===lt.Right?f.scrollLeft-y.x:f.scrollLeft-R.x,_.x&&f.scrollBy({left:-_.x,behavior:l});break}else if(F&&T.y!==h.y){const P=f.scrollTop+m.y,V=p===lt.Down&&P<=y.y||p===lt.Up&&P>=R.y;if(V&&!m.x){f.scrollTo({top:P,behavior:l});return}V?_.y=f.scrollTop-P:_.y=p===lt.Down?f.scrollTop-y.y:f.scrollTop-R.y,_.y&&f.scrollBy({top:-_.y,behavior:l});break}}this.handleMove(t,ys(yi(h,this.referenceCoordinates),_))}}}handleMove(t,r){const{onMove:n}=this.props;t.preventDefault(),n(r)}handleEnd(t){const{onEnd:r}=this.props;t.preventDefault(),this.detach(),r()}handleCancel(t){const{onCancel:r}=this.props;t.preventDefault(),this.detach(),r()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}s0.activators=[{eventName:"onKeyDown",handler:(e,t,r)=>{let{keyboardCodes:n=n0,onActivation:s}=t,{active:i}=r;const{code:o}=e.nativeEvent;if(n.start.includes(o)){const l=i.activatorNode.current;return l&&e.target!==l?!1:(e.preventDefault(),s==null||s({event:e.nativeEvent}),!0)}return!1}}];function Sc(e){return!!(e&&"distance"in e)}function Tc(e){return!!(e&&"delay"in e)}class El{constructor(t,r,n){var s;n===void 0&&(n=Zx(t.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=t,this.events=r;const{event:i}=t,{target:o}=i;this.props=t,this.events=r,this.document=Rs(o),this.documentListeners=new Qs(this.document),this.listeners=new Qs(n),this.windowListeners=new Qs(_r(o)),this.initialCoordinates=(s=Qo(i))!=null?s:Kr,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:t,props:{options:{activationConstraint:r,bypassActivationConstraint:n}}}=this;if(this.listeners.add(t.move.name,this.handleMove,{passive:!1}),this.listeners.add(t.end.name,this.handleEnd),t.cancel&&this.listeners.add(t.cancel.name,this.handleCancel),this.windowListeners.add(Lr.Resize,this.handleCancel),this.windowListeners.add(Lr.DragStart,wc),this.windowListeners.add(Lr.VisibilityChange,this.handleCancel),this.windowListeners.add(Lr.ContextMenu,wc),this.documentListeners.add(Lr.Keydown,this.handleKeydown),r){if(n!=null&&n({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(Tc(r)){this.timeoutId=setTimeout(this.handleStart,r.delay),this.handlePending(r);return}if(Sc(r)){this.handlePending(r);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(t,r){const{active:n,onPending:s}=this.props;s(n,t,this.initialCoordinates,r)}handleStart(){const{initialCoordinates:t}=this,{onStart:r}=this.props;t&&(this.activated=!0,this.documentListeners.add(Lr.Click,Qx,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(Lr.SelectionChange,this.removeTextSelection),r(t))}handleMove(t){var r;const{activated:n,initialCoordinates:s,props:i}=this,{onMove:o,options:{activationConstraint:l}}=i;if(!s)return;const c=(r=Qo(t))!=null?r:Kr,d=yi(s,c);if(!n&&l){if(Sc(l)){if(l.tolerance!=null&&So(d,l.tolerance))return this.handleCancel();if(So(d,l.distance))return this.handleStart()}if(Tc(l)&&So(d,l.tolerance))return this.handleCancel();this.handlePending(l,d);return}t.cancelable&&t.preventDefault(),o(c)}handleEnd(){const{onAbort:t,onEnd:r}=this.props;this.detach(),this.activated||t(this.props.active),r()}handleCancel(){const{onAbort:t,onCancel:r}=this.props;this.detach(),this.activated||t(this.props.active),r()}handleKeydown(t){t.code===lt.Esc&&this.handleCancel()}removeTextSelection(){var t;(t=this.document.getSelection())==null||t.removeAllRanges()}}const tp={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class jl extends El{constructor(t){const{event:r}=t,n=Rs(r.target);super(t,tp,n)}}jl.activators=[{eventName:"onPointerDown",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;return!r.isPrimary||r.button!==0?!1:(n==null||n({event:r}),!0)}}];const rp={move:{name:"mousemove"},end:{name:"mouseup"}};var tl;(function(e){e[e.RightClick=2]="RightClick"})(tl||(tl={}));class np extends El{constructor(t){super(t,rp,Rs(t.event.target))}}np.activators=[{eventName:"onMouseDown",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;return r.button===tl.RightClick?!1:(n==null||n({event:r}),!0)}}];const To={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class sp extends El{constructor(t){super(t,To)}static setup(){return window.addEventListener(To.move.name,t,{capture:!1,passive:!1}),function(){window.removeEventListener(To.move.name,t)};function t(){}}}sp.activators=[{eventName:"onTouchStart",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;const{touches:s}=r;return s.length>1?!1:(n==null||n({event:r}),!0)}}];var ea;(function(e){e[e.Pointer=0]="Pointer",e[e.DraggableRect=1]="DraggableRect"})(ea||(ea={}));var Si;(function(e){e[e.TreeOrder=0]="TreeOrder",e[e.ReversedTreeOrder=1]="ReversedTreeOrder"})(Si||(Si={}));function ap(e){let{acceleration:t,activator:r=ea.Pointer,canScroll:n,draggingRect:s,enabled:i,interval:o=5,order:l=Si.TreeOrder,pointerCoordinates:c,scrollableAncestors:d,scrollableAncestorRects:u,delta:h,threshold:m}=e;const _=op({delta:h,disabled:!i}),[x,f]=mx(),p=g.useRef({x:0,y:0}),I=g.useRef({x:0,y:0}),S=g.useMemo(()=>{switch(r){case ea.Pointer:return c?{top:c.y,bottom:c.y,left:c.x,right:c.x}:null;case ea.DraggableRect:return s}},[r,s,c]),w=g.useRef(null),v=g.useCallback(()=>{const R=w.current;if(!R)return;const j=p.current.x*I.current.x,T=p.current.y*I.current.y;R.scrollBy(j,T)},[]),y=g.useMemo(()=>l===Si.TreeOrder?[...d].reverse():d,[l,d]);g.useEffect(()=>{if(!i||!d.length||!S){f();return}for(const R of y){if((n==null?void 0:n(R))===!1)continue;const j=d.indexOf(R),T=u[j];if(!T)continue;const{direction:C,speed:F}=Kx(R,T,S,t,m);for(const P of["x","y"])_[P][C[P]]||(F[P]=0,C[P]=0);if(F.x>0||F.y>0){f(),w.current=R,x(v,o),p.current=F,I.current=C;return}}p.current={x:0,y:0},I.current={x:0,y:0},f()},[t,v,n,f,i,o,JSON.stringify(S),JSON.stringify(_),x,d,y,u,JSON.stringify(m)])}const ip={x:{[Bt.Backward]:!1,[Bt.Forward]:!1},y:{[Bt.Backward]:!1,[Bt.Forward]:!1}};function op(e){let{delta:t,disabled:r}=e;const n=Zo(t);return Ia(s=>{if(r||!n||!s)return ip;const i={x:Math.sign(t.x-n.x),y:Math.sign(t.y-n.y)};return{x:{[Bt.Backward]:s.x[Bt.Backward]||i.x===-1,[Bt.Forward]:s.x[Bt.Forward]||i.x===1},y:{[Bt.Backward]:s.y[Bt.Backward]||i.y===-1,[Bt.Forward]:s.y[Bt.Forward]||i.y===1}}},[r,t,n])}function lp(e,t){const r=t!=null?e.get(t):void 0,n=r?r.node.current:null;return Ia(s=>{var i;return t==null?null:(i=n??s)!=null?i:null},[n,t])}function cp(e,t){return g.useMemo(()=>e.reduce((r,n)=>{const{sensor:s}=n,i=s.activators.map(o=>({eventName:o.eventName,handler:t(o.handler,n)}));return[...r,...i]},[]),[e,t])}var xa;(function(e){e[e.Always=0]="Always",e[e.BeforeDragging=1]="BeforeDragging",e[e.WhileDragging=2]="WhileDragging"})(xa||(xa={}));var rl;(function(e){e.Optimized="optimized"})(rl||(rl={}));const bc=new Map;function dp(e,t){let{dragging:r,dependencies:n,config:s}=t;const[i,o]=g.useState(null),{frequency:l,measure:c,strategy:d}=s,u=g.useRef(e),h=p(),m=ha(h),_=g.useCallback(function(I){I===void 0&&(I=[]),!m.current&&o(S=>S===null?I:S.concat(I.filter(w=>!S.includes(w))))},[m]),x=g.useRef(null),f=Ia(I=>{if(h&&!r)return bc;if(!I||I===bc||u.current!==e||i!=null){const S=new Map;for(let w of e){if(!w)continue;if(i&&i.length>0&&!i.includes(w.id)&&w.rect.current){S.set(w.id,w.rect.current);continue}const v=w.node.current,y=v?new bl(c(v),v):null;w.rect.current=y,y&&S.set(w.id,y)}return S}return I},[e,i,r,h,c]);return g.useEffect(()=>{u.current=e},[e]),g.useEffect(()=>{h||_()},[r,h]),g.useEffect(()=>{i&&i.length>0&&o(null)},[JSON.stringify(i)]),g.useEffect(()=>{h||typeof l!="number"||x.current!==null||(x.current=setTimeout(()=>{_(),x.current=null},l))},[l,h,_,...n]),{droppableRects:f,measureDroppableContainers:_,measuringScheduled:i!=null};function p(){switch(d){case xa.Always:return!1;case xa.BeforeDragging:return r;default:return!r}}}function a0(e,t){return Ia(r=>e?r||(typeof t=="function"?t(e):e):null,[t,e])}function up(e,t){return a0(e,t)}function fp(e){let{callback:t,disabled:r}=e;const n=wl(t),s=g.useMemo(()=>{if(r||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:i}=window;return new i(n)},[n,r]);return g.useEffect(()=>()=>s==null?void 0:s.disconnect(),[s]),s}function Qi(e){let{callback:t,disabled:r}=e;const n=wl(t),s=g.useMemo(()=>{if(r||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:i}=window;return new i(n)},[r]);return g.useEffect(()=>()=>s==null?void 0:s.disconnect(),[s]),s}function hp(e){return new bl(Ds(e),e)}function Ec(e,t,r){t===void 0&&(t=hp);const[n,s]=g.useState(null);function i(){s(c=>{if(!e)return null;if(e.isConnected===!1){var d;return(d=c??r)!=null?d:null}const u=t(e);return JSON.stringify(c)===JSON.stringify(u)?c:u})}const o=fp({callback(c){if(e)for(const d of c){const{type:u,target:h}=d;if(u==="childList"&&h instanceof HTMLElement&&h.contains(e)){i();break}}}}),l=Qi({callback:i});return sn(()=>{i(),e?(l==null||l.observe(e),o==null||o.observe(document.body,{childList:!0,subtree:!0})):(l==null||l.disconnect(),o==null||o.disconnect())},[e]),n}function mp(e){const t=a0(e);return Xu(e,t)}const jc=[];function xp(e){const t=g.useRef(e),r=Ia(n=>e?n&&n!==jc&&e&&t.current&&e.parentNode===t.current.parentNode?n:Tl(e):jc,[e]);return g.useEffect(()=>{t.current=e},[e]),r}function pp(e){const[t,r]=g.useState(null),n=g.useRef(e),s=g.useCallback(i=>{const o=wo(i.target);o&&r(l=>l?(l.set(o,el(o)),new Map(l)):null)},[]);return g.useEffect(()=>{const i=n.current;if(e!==i){o(i);const l=e.map(c=>{const d=wo(c);return d?(d.addEventListener("scroll",s,{passive:!0}),[d,el(d)]):null}).filter(c=>c!=null);r(l.length?new Map(l):null),n.current=e}return()=>{o(e),o(i)};function o(l){l.forEach(c=>{const d=wo(c);d==null||d.removeEventListener("scroll",s)})}},[s,e]),g.useMemo(()=>e.length?t?Array.from(t.values()).reduce((i,o)=>ys(i,o),Kr):r0(e):Kr,[e,t])}function Ic(e,t){t===void 0&&(t=[]);const r=g.useRef(null);return g.useEffect(()=>{r.current=null},t),g.useEffect(()=>{const n=e!==Kr;n&&!r.current&&(r.current=e),!n&&r.current&&(r.current=null)},[e]),r.current?yi(e,r.current):Kr}function gp(e){g.useEffect(()=>{if(!Zi)return;const t=e.map(r=>{let{sensor:n}=r;return n.setup==null?void 0:n.setup()});return()=>{for(const r of t)r==null||r()}},e.map(t=>{let{sensor:r}=t;return r}))}function _p(e,t){return g.useMemo(()=>e.reduce((r,n)=>{let{eventName:s,handler:i}=n;return r[s]=o=>{i(o,t)},r},{}),[e,t])}function i0(e){return g.useMemo(()=>e?$x(e):null,[e])}const Cc=[];function vp(e,t){t===void 0&&(t=Ds);const[r]=e,n=i0(r?_r(r):null),[s,i]=g.useState(Cc);function o(){i(()=>e.length?e.map(c=>e0(c)?n:new bl(t(c),c)):Cc)}const l=Qi({callback:o});return sn(()=>{l==null||l.disconnect(),o(),e.forEach(c=>l==null?void 0:l.observe(c))},[e]),s}function yp(e){if(!e)return null;if(e.children.length>1)return e;const t=e.children[0];return ja(t)?t:e}function wp(e){let{measure:t}=e;const[r,n]=g.useState(null),s=g.useCallback(d=>{for(const{target:u}of d)if(ja(u)){n(h=>{const m=t(u);return h?{...h,width:m.width,height:m.height}:m});break}},[t]),i=Qi({callback:s}),o=g.useCallback(d=>{const u=yp(d);i==null||i.disconnect(),u&&(i==null||i.observe(u)),n(u?t(u):null)},[t,i]),[l,c]=vi(o);return g.useMemo(()=>({nodeRef:l,rect:r,setRef:c}),[r,l,c])}const Sp=[{sensor:jl,options:{}},{sensor:s0,options:{}}],Tp={current:{}},di={draggable:{measure:yc},droppable:{measure:yc,strategy:xa.WhileDragging,frequency:rl.Optimized},dragOverlay:{measure:Ds}};class ta extends Map{get(t){var r;return t!=null&&(r=super.get(t))!=null?r:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(t=>{let{disabled:r}=t;return!r})}getNodeFor(t){var r,n;return(r=(n=this.get(t))==null?void 0:n.node.current)!=null?r:void 0}}const bp={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new ta,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:wi},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:di,measureDroppableContainers:wi,windowRect:null,measuringScheduled:!1},Ep={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:wi,draggableNodes:new Map,over:null,measureDroppableContainers:wi},eo=g.createContext(Ep),o0=g.createContext(bp);function jp(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new ta}}}function Ip(e,t){switch(t.type){case Mt.DragStart:return{...e,draggable:{...e.draggable,initialCoordinates:t.initialCoordinates,active:t.active}};case Mt.DragMove:return e.draggable.active==null?e:{...e,draggable:{...e.draggable,translate:{x:t.coordinates.x-e.draggable.initialCoordinates.x,y:t.coordinates.y-e.draggable.initialCoordinates.y}}};case Mt.DragEnd:case Mt.DragCancel:return{...e,draggable:{...e.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case Mt.RegisterDroppable:{const{element:r}=t,{id:n}=r,s=new ta(e.droppable.containers);return s.set(n,r),{...e,droppable:{...e.droppable,containers:s}}}case Mt.SetDroppableDisabled:{const{id:r,key:n,disabled:s}=t,i=e.droppable.containers.get(r);if(!i||n!==i.key)return e;const o=new ta(e.droppable.containers);return o.set(r,{...i,disabled:s}),{...e,droppable:{...e.droppable,containers:o}}}case Mt.UnregisterDroppable:{const{id:r,key:n}=t,s=e.droppable.containers.get(r);if(!s||n!==s.key)return e;const i=new ta(e.droppable.containers);return i.delete(r),{...e,droppable:{...e.droppable,containers:i}}}default:return e}}function Cp(e){let{disabled:t}=e;const{active:r,activatorEvent:n,draggableNodes:s}=g.useContext(eo),i=Zo(n),o=Zo(r==null?void 0:r.id);return g.useEffect(()=>{if(!t&&!n&&i&&o!=null){if(!Sl(i)||document.activeElement===i.target)return;const l=s.get(o);if(!l)return;const{activatorNode:c,node:d}=l;if(!c.current&&!d.current)return;requestAnimationFrame(()=>{for(const u of[c.current,d.current]){if(!u)continue;const h=gx(u);if(h){h.focus();break}}})}},[n,t,s,o,i]),null}function Ap(e,t){let{transform:r,...n}=t;return e!=null&&e.length?e.reduce((s,i)=>i({transform:s,...n}),r):r}function kp(e){return g.useMemo(()=>({draggable:{...di.draggable,...e==null?void 0:e.draggable},droppable:{...di.droppable,...e==null?void 0:e.droppable},dragOverlay:{...di.dragOverlay,...e==null?void 0:e.dragOverlay}}),[e==null?void 0:e.draggable,e==null?void 0:e.droppable,e==null?void 0:e.dragOverlay])}function Fp(e){let{activeNode:t,measure:r,initialRect:n,config:s=!0}=e;const i=g.useRef(!1),{x:o,y:l}=typeof s=="boolean"?{x:s,y:s}:s;sn(()=>{if(!o&&!l||!t){i.current=!1;return}if(i.current||!n)return;const d=t==null?void 0:t.node.current;if(!d||d.isConnected===!1)return;const u=r(d),h=Xu(u,n);if(o||(h.x=0),l||(h.y=0),i.current=!0,Math.abs(h.x)>0||Math.abs(h.y)>0){const m=Ju(d);m&&m.scrollBy({top:h.y,left:h.x})}},[t,o,l,n,r])}const l0=g.createContext({...Kr,scaleX:1,scaleY:1});var kn;(function(e){e[e.Uninitialized=0]="Uninitialized",e[e.Initializing=1]="Initializing",e[e.Initialized=2]="Initialized"})(kn||(kn={}));const Op=g.memo(function(t){var r,n,s,i;let{id:o,accessibility:l,autoScroll:c=!0,children:d,sensors:u=Sp,collisionDetection:h=Px,measuring:m,modifiers:_,...x}=t;const f=g.useReducer(Ip,void 0,jp),[p,I]=f,[S,w]=Tx(),[v,y]=g.useState(kn.Uninitialized),R=v===kn.Initialized,{draggable:{active:j,nodes:T,translate:C},droppable:{containers:F}}=p,P=j!=null?T.get(j):null,V=g.useRef({initial:null,translated:null}),X=g.useMemo(()=>{var St;return j!=null?{id:j,data:(St=P==null?void 0:P.data)!=null?St:Tp,rect:V}:null},[j,P]),_e=g.useRef(null),[B,te]=g.useState(null),[J,G]=g.useState(null),se=ha(x,Object.values(x)),Q=Ca("DndDescribedBy",o),de=g.useMemo(()=>F.getEnabled(),[F]),k=kp(m),{droppableRects:W,measureDroppableContainers:z,measuringScheduled:D}=dp(de,{dragging:R,dependencies:[C.x,C.y],config:k.droppable}),q=lp(T,j),K=g.useMemo(()=>J?Qo(J):null,[J]),Z=En(),me=up(q,k.draggable.measure);Fp({activeNode:j!=null?T.get(j):null,config:Z.layoutShiftCompensation,initialRect:me,measure:k.draggable.measure});const ie=Ec(q,k.draggable.measure,me),he=Ec(q?q.parentElement:null),we=g.useRef({activatorEvent:null,active:null,activeNode:q,collisionRect:null,collisions:null,droppableRects:W,draggableNodes:T,draggingNode:null,draggingNodeRect:null,droppableContainers:F,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),He=F.getNodeFor((r=we.current.over)==null?void 0:r.id),ze=wp({measure:k.dragOverlay.measure}),gt=(n=ze.nodeRef.current)!=null?n:q,qe=R?(s=ze.rect)!=null?s:ie:null,le=!!(ze.nodeRef.current&&ze.rect),fe=mp(le?null:ie),Re=i0(gt?_r(gt):null),Ue=xp(R?He??q:null),nt=vp(Ue),Tt=Ap(_,{transform:{x:C.x-fe.x,y:C.y-fe.y,scaleX:1,scaleY:1},activatorEvent:J,active:X,activeNodeRect:ie,containerNodeRect:he,draggingNodeRect:qe,over:we.current.over,overlayNodeRect:ze.rect,scrollableAncestors:Ue,scrollableAncestorRects:nt,windowRect:Re}),yt=K?ys(K,C):null,De=pp(Ue),Ce=Ic(De),Qe=Ic(De,[ie]),Xe=ys(Tt,Ce),Je=qe?Mx(qe,Tt):null,xe=X&&Je?h({active:X,collisionRect:Je,droppableRects:W,droppableContainers:de,pointerCoordinates:yt}):null,ke=Ox(xe,"id"),[et,_t]=g.useState(null),Wt=le?Tt:ys(Tt,Qe),Sn=Nx(Wt,(i=et==null?void 0:et.rect)!=null?i:null,ie),cr=g.useRef(null),as=g.useCallback((St,st)=>{let{sensor:kt,options:dr}=st;if(_e.current==null)return;const Qt=T.get(_e.current);if(!Qt)return;const Ht=St.nativeEvent,ur=new kt({active:_e.current,activeNode:Qt,event:Ht,options:dr,context:we,onAbort(It){if(!T.get(It))return;const{onDragAbort:er}=se.current,vr={id:It};er==null||er(vr),S({type:"onDragAbort",event:vr})},onPending(It,Fr,er,vr){if(!T.get(It))return;const{onDragPending:ln}=se.current,A={id:It,constraint:Fr,initialCoordinates:er,offset:vr};ln==null||ln(A),S({type:"onDragPending",event:A})},onStart(It){const Fr=_e.current;if(Fr==null)return;const er=T.get(Fr);if(!er)return;const{onDragStart:vr}=se.current,on={activatorEvent:Ht,active:{id:Fr,data:er.data,rect:V}};Vn.unstable_batchedUpdates(()=>{vr==null||vr(on),y(kn.Initializing),I({type:Mt.DragStart,initialCoordinates:It,active:Fr}),S({type:"onDragStart",event:on}),te(cr.current),G(Ht)})},onMove(It){I({type:Mt.DragMove,coordinates:It})},onEnd:Xr(Mt.DragEnd),onCancel:Xr(Mt.DragCancel)});cr.current=ur;function Xr(It){return async function(){const{active:er,collisions:vr,over:on,scrollAdjustedTranslate:ln}=we.current;let A=null;if(er&&ln){const{cancelDrop:L}=se.current;A={activatorEvent:Ht,active:er,collisions:vr,delta:ln,over:on},It===Mt.DragEnd&&typeof L=="function"&&await Promise.resolve(L(A))&&(It=Mt.DragCancel)}_e.current=null,Vn.unstable_batchedUpdates(()=>{I({type:It}),y(kn.Uninitialized),_t(null),te(null),G(null),cr.current=null;const L=It===Mt.DragEnd?"onDragEnd":"onDragCancel";if(A){const O=se.current[L];O==null||O(A),S({type:L,event:A})}})}}},[T]),Tn=g.useCallback((St,st)=>(kt,dr)=>{const Qt=kt.nativeEvent,Ht=T.get(dr);if(_e.current!==null||!Ht||Qt.dndKit||Qt.defaultPrevented)return;const ur={active:Ht};St(kt,st.options,ur)===!0&&(Qt.dndKit={capturedBy:st.sensor},_e.current=dr,as(kt,st))},[T,as]),kr=cp(u,Tn);gp(u),sn(()=>{ie&&v===kn.Initializing&&y(kn.Initialized)},[ie,v]),g.useEffect(()=>{const{onDragMove:St}=se.current,{active:st,activatorEvent:kt,collisions:dr,over:Qt}=we.current;if(!st||!kt)return;const Ht={active:st,activatorEvent:kt,collisions:dr,delta:{x:Xe.x,y:Xe.y},over:Qt};Vn.unstable_batchedUpdates(()=>{St==null||St(Ht),S({type:"onDragMove",event:Ht})})},[Xe.x,Xe.y]),g.useEffect(()=>{const{active:St,activatorEvent:st,collisions:kt,droppableContainers:dr,scrollAdjustedTranslate:Qt}=we.current;if(!St||_e.current==null||!st||!Qt)return;const{onDragOver:Ht}=se.current,ur=dr.get(ke),Xr=ur&&ur.rect.current?{id:ur.id,rect:ur.rect.current,data:ur.data,disabled:ur.disabled}:null,It={active:St,activatorEvent:st,collisions:kt,delta:{x:Qt.x,y:Qt.y},over:Xr};Vn.unstable_batchedUpdates(()=>{_t(Xr),Ht==null||Ht(It),S({type:"onDragOver",event:It})})},[ke]),sn(()=>{we.current={activatorEvent:J,active:X,activeNode:q,collisionRect:Je,collisions:xe,droppableRects:W,draggableNodes:T,draggingNode:gt,draggingNodeRect:qe,droppableContainers:F,over:et,scrollableAncestors:Ue,scrollAdjustedTranslate:Xe},V.current={initial:qe,translated:Je}},[X,q,xe,Je,T,gt,qe,W,F,et,Ue,Xe]),ap({...Z,delta:C,draggingRect:Je,pointerCoordinates:yt,scrollableAncestors:Ue,scrollableAncestorRects:nt});const qr=g.useMemo(()=>({active:X,activeNode:q,activeNodeRect:ie,activatorEvent:J,collisions:xe,containerNodeRect:he,dragOverlay:ze,draggableNodes:T,droppableContainers:F,droppableRects:W,over:et,measureDroppableContainers:z,scrollableAncestors:Ue,scrollableAncestorRects:nt,measuringConfiguration:k,measuringScheduled:D,windowRect:Re}),[X,q,ie,J,xe,he,ze,T,F,W,et,z,Ue,nt,k,D,Re]),bn=g.useMemo(()=>({activatorEvent:J,activators:kr,active:X,activeNodeRect:ie,ariaDescribedById:{draggable:Q},dispatch:I,draggableNodes:T,over:et,measureDroppableContainers:z}),[J,kr,X,ie,I,Q,T,et,z]);return Gt.createElement(qu.Provider,{value:w},Gt.createElement(eo.Provider,{value:bn},Gt.createElement(o0.Provider,{value:qr},Gt.createElement(l0.Provider,{value:Sn},d)),Gt.createElement(Cp,{disabled:(l==null?void 0:l.restoreFocus)===!1})),Gt.createElement(jx,{...l,hiddenTextDescribedById:Q}));function En(){const St=(B==null?void 0:B.autoScrollEnabled)===!1,st=typeof c=="object"?c.enabled===!1:c===!1,kt=R&&!St&&!st;return typeof c=="object"?{...c,enabled:kt}:{enabled:kt}}}),Rp=g.createContext(null),Ac="button",Dp="Draggable";function Pp(e){let{id:t,data:r,disabled:n=!1,attributes:s}=e;const i=Ca(Dp),{activators:o,activatorEvent:l,active:c,activeNodeRect:d,ariaDescribedById:u,draggableNodes:h,over:m}=g.useContext(eo),{role:_=Ac,roleDescription:x="draggable",tabIndex:f=0}=s??{},p=(c==null?void 0:c.id)===t,I=g.useContext(p?l0:Rp),[S,w]=vi(),[v,y]=vi(),R=_p(o,t),j=ha(r);sn(()=>(h.set(t,{id:t,key:i,node:S,activatorNode:v,data:j}),()=>{const C=h.get(t);C&&C.key===i&&h.delete(t)}),[h,t]);const T=g.useMemo(()=>({role:_,tabIndex:f,"aria-disabled":n,"aria-pressed":p&&_===Ac?!0:void 0,"aria-roledescription":x,"aria-describedby":u.draggable}),[n,_,f,p,x,u.draggable]);return{active:c,activatorEvent:l,activeNodeRect:d,attributes:T,isDragging:p,listeners:n?void 0:R,node:S,over:m,setNodeRef:w,setActivatorNodeRef:y,transform:I}}function Np(){return g.useContext(o0)}const Lp="Droppable",Mp={timeout:25};function Bp(e){let{data:t,disabled:r=!1,id:n,resizeObserverConfig:s}=e;const i=Ca(Lp),{active:o,dispatch:l,over:c,measureDroppableContainers:d}=g.useContext(eo),u=g.useRef({disabled:r}),h=g.useRef(!1),m=g.useRef(null),_=g.useRef(null),{disabled:x,updateMeasurementsFor:f,timeout:p}={...Mp,...s},I=ha(f??n),S=g.useCallback(()=>{if(!h.current){h.current=!0;return}_.current!=null&&clearTimeout(_.current),_.current=setTimeout(()=>{d(Array.isArray(I.current)?I.current:[I.current]),_.current=null},p)},[p]),w=Qi({callback:S,disabled:x||!o}),v=g.useCallback((T,C)=>{w&&(C&&(w.unobserve(C),h.current=!1),T&&w.observe(T))},[w]),[y,R]=vi(v),j=ha(t);return g.useEffect(()=>{!w||!y.current||(w.disconnect(),h.current=!1,w.observe(y.current))},[y,w]),g.useEffect(()=>(l({type:Mt.RegisterDroppable,element:{id:n,key:i,disabled:r,node:y,rect:m,data:j}}),()=>l({type:Mt.UnregisterDroppable,key:i,id:n})),[n]),g.useEffect(()=>{r!==u.current.disabled&&(l({type:Mt.SetDroppableDisabled,id:n,key:i,disabled:r}),u.current.disabled=r)},[n,i,r,l]),{active:o,rect:m,isOver:(c==null?void 0:c.id)===n,node:y,over:c,setNodeRef:R}}function Il(e,t,r){const n=e.slice();return n.splice(r<0?n.length+r:r,0,n.splice(t,1)[0]),n}function Up(e,t){return e.reduce((r,n,s)=>{const i=t.get(n);return i&&(r[s]=i),r},Array(e.length))}function Ua(e){return e!==null&&e>=0}function zp(e,t){if(e===t)return!0;if(e.length!==t.length)return!1;for(let r=0;r{var t;let{rects:r,activeNodeRect:n,activeIndex:s,overIndex:i,index:o}=e;const l=(t=r[s])!=null?t:n;if(!l)return null;const c=Hp(r,o,s);if(o===s){const d=r[i];return d?{x:ss&&o<=i?{x:-l.width-c,y:0,...za}:o=i?{x:l.width+c,y:0,...za}:{x:0,y:0,...za}};function Hp(e,t,r){const n=e[t],s=e[t-1],i=e[t+1];return!n||!s&&!i?0:r{let{rects:t,activeIndex:r,overIndex:n,index:s}=e;const i=Il(t,n,r),o=t[s],l=i[s];return!l||!o?null:{x:l.left-o.left,y:l.top-o.top,scaleX:l.width/o.width,scaleY:l.height/o.height}},d0="Sortable",u0=Gt.createContext({activeIndex:-1,containerId:d0,disableTransforms:!1,items:[],overIndex:-1,useDragOverlay:!1,sortedRects:[],strategy:c0,disabled:{draggable:!1,droppable:!1}});function Vp(e){let{children:t,id:r,items:n,strategy:s=c0,disabled:i=!1}=e;const{active:o,dragOverlay:l,droppableRects:c,over:d,measureDroppableContainers:u}=Np(),h=Ca(d0,r),m=l.rect!==null,_=g.useMemo(()=>n.map(R=>typeof R=="object"&&"id"in R?R.id:R),[n]),x=o!=null,f=o?_.indexOf(o.id):-1,p=d?_.indexOf(d.id):-1,I=g.useRef(_),S=!zp(_,I.current),w=p!==-1&&f===-1||S,v=$p(i);sn(()=>{S&&x&&u(_)},[S,_,x,u]),g.useEffect(()=>{I.current=_},[_]);const y=g.useMemo(()=>({activeIndex:f,containerId:h,disabled:v,disableTransforms:w,items:_,overIndex:p,useDragOverlay:m,sortedRects:Up(_,c),strategy:s}),[f,h,v.draggable,v.droppable,w,_,p,c,m,s]);return Gt.createElement(u0.Provider,{value:y},t)}const Kp=e=>{let{id:t,items:r,activeIndex:n,overIndex:s}=e;return Il(r,n,s).indexOf(t)},Gp=e=>{let{containerId:t,isSorting:r,wasDragging:n,index:s,items:i,newIndex:o,previousItems:l,previousContainerId:c,transition:d}=e;return!d||!n||l!==i&&s===o?!1:r?!0:o!==s&&t===c},Yp={duration:200,easing:"ease"},f0="transform",qp=ma.Transition.toString({property:f0,duration:0,easing:"linear"}),Xp={roleDescription:"sortable"};function Jp(e){let{disabled:t,index:r,node:n,rect:s}=e;const[i,o]=g.useState(null),l=g.useRef(r);return sn(()=>{if(!t&&r!==l.current&&n.current){const c=s.current;if(c){const d=Ds(n.current,{ignoreTransform:!0}),u={x:c.left-d.left,y:c.top-d.top,scaleX:c.width/d.width,scaleY:c.height/d.height};(u.x||u.y)&&o(u)}}r!==l.current&&(l.current=r)},[t,r,n,s]),g.useEffect(()=>{i&&o(null)},[i]),i}function Zp(e){let{animateLayoutChanges:t=Gp,attributes:r,disabled:n,data:s,getNewIndex:i=Kp,id:o,strategy:l,resizeObserverConfig:c,transition:d=Yp}=e;const{items:u,containerId:h,activeIndex:m,disabled:_,disableTransforms:x,sortedRects:f,overIndex:p,useDragOverlay:I,strategy:S}=g.useContext(u0),w=Qp(n,_),v=u.indexOf(o),y=g.useMemo(()=>({sortable:{containerId:h,index:v,items:u},...s}),[h,s,v,u]),R=g.useMemo(()=>u.slice(u.indexOf(o)),[u,o]),{rect:j,node:T,isOver:C,setNodeRef:F}=Bp({id:o,data:y,disabled:w.droppable,resizeObserverConfig:{updateMeasurementsFor:R,...c}}),{active:P,activatorEvent:V,activeNodeRect:X,attributes:_e,setNodeRef:B,listeners:te,isDragging:J,over:G,setActivatorNodeRef:se,transform:Q}=Pp({id:o,data:y,attributes:{...Xp,...r},disabled:w.draggable}),de=hx(F,B),k=!!P,W=k&&!x&&Ua(m)&&Ua(p),z=!I&&J,D=z&&W?Q:null,K=W?D??(l??S)({rects:f,activeNodeRect:X,activeIndex:m,overIndex:p,index:v}):null,Z=Ua(m)&&Ua(p)?i({id:o,items:u,activeIndex:m,overIndex:p}):v,me=P==null?void 0:P.id,ie=g.useRef({activeId:me,items:u,newIndex:Z,containerId:h}),he=u!==ie.current.items,we=t({active:P,containerId:h,isDragging:J,isSorting:k,id:o,index:v,items:u,newIndex:ie.current.newIndex,previousItems:ie.current.items,previousContainerId:ie.current.containerId,transition:d,wasDragging:ie.current.activeId!=null}),He=Jp({disabled:!we,index:v,node:T,rect:j});return g.useEffect(()=>{k&&ie.current.newIndex!==Z&&(ie.current.newIndex=Z),h!==ie.current.containerId&&(ie.current.containerId=h),u!==ie.current.items&&(ie.current.items=u)},[k,Z,h,u]),g.useEffect(()=>{if(me===ie.current.activeId)return;if(me!=null&&ie.current.activeId==null){ie.current.activeId=me;return}const gt=setTimeout(()=>{ie.current.activeId=me},50);return()=>clearTimeout(gt)},[me]),{active:P,activeIndex:m,attributes:_e,data:y,rect:j,index:v,newIndex:Z,items:u,isOver:C,isSorting:k,isDragging:J,listeners:te,node:T,overIndex:p,over:G,setNodeRef:de,setActivatorNodeRef:se,setDroppableNodeRef:F,setDraggableNodeRef:B,transform:He??K,transition:ze()};function ze(){if(He||he&&ie.current.newIndex===v)return qp;if(!(z&&!Sl(V)||!d)&&(k||we))return ma.Transition.toString({...d,property:f0})}}function Qp(e,t){var r,n;return typeof e=="boolean"?{draggable:e,droppable:!1}:{draggable:(r=e==null?void 0:e.draggable)!=null?r:t.draggable,droppable:(n=e==null?void 0:e.droppable)!=null?n:t.droppable}}lt.Down,lt.Right,lt.Up,lt.Left;function Cl(e){return e==null}function h0(e){return Object.prototype.toString.call(e)==="[object RegExp]"}function nl(e){return Array.isArray(e)}function sl(e){return typeof e=="function"}function m0(e){return{set:(t,r)=>(e.setAttribute(t,r),m0(e))}}function kc(e=100){let t;return new Promise((r,n)=>{t=setTimeout(()=>{r(),Cl(t)||clearTimeout(t)},e)})}function Fc(e,t){return(nl(e)?e:Cl(e)?[]:[e]).some(r=>h0(r)?r.test(t):t===r)}function bo(e){setTimeout(e,0)}var e1=g.createContext({active:!1,refresh:()=>{},destroy:()=>Promise.resolve(),destroyAll:()=>Promise.resolve(),destroyOther:()=>Promise.resolve(),getCacheNodes:()=>[]}),t1=g.memo(function(e){let{children:t,active:r,refresh:n,destroy:s,destroyAll:i,destroyOther:o,getCacheNodes:l}=e,c=g.useMemo(()=>({active:r,refresh:n,destroy:s,destroyAll:i,destroyOther:o,getCacheNodes:l}),[r,n,s,i,o,l]);return a.jsx(e1.Provider,{value:c,children:t})}),r1=t1,x0=g.Activity,n1=x0??g.Fragment,s1=!!x0,p0="keepalive-cache-div";function g0(e){return e?Array.from(e.children):[]}function al(e){e.forEach(t=>{t.classList.contains(p0)&&t.remove()})}function Oc(e,t){let r=g0(e);al(r),e.appendChild(t),t.classList.remove("inactive"),t.classList.add("active")}function Rc(e,t){let r=g0(e).filter(n=>n.classList.contains("active")&&n.getAttribute("data-cache-key")!==t);return r.forEach(n=>{n.classList.remove("active"),n.classList.add("inactive")}),r}function a1(e,t,r){return r?Fc(r,e):t?!Fc(t,e):!0}var i1=g.memo(function(e){let{errorElement:t=g.Fragment,cacheNodeClassName:r,children:n,cacheKey:s,exclude:i,include:o,enableActivity:l}=e,{active:c,renderCount:d,destroy:u,transition:h,viewTransition:m,duration:_,containerDivRef:x}=e,f=g.useRef(!1);f.current=f.current||c;let p=g.useMemo(()=>{let I=document.createElement("div");return m0(I).set("data-cache-key",s).set("style","height: 100%").set("data-render-count",d.toString()),I.className=p0+(r?` ${r}`:""),I},[d,r]);return g.useEffect(()=>{let I=a1(s,i,o),S=x.current;if(!S){console.warn("keepalive: cache container not found");return}if(h)(async()=>{if(c){let w=Rc(S,s);if(await kc(_-40),al(w),S.contains(p))return;Oc(S,p)}else I||(await kc(_),u(s))})();else if(c){let w=()=>{let v=Rc(S,s);al(v),!S.contains(p)&&Oc(S,p)};m&&document.startViewTransition?document.startViewTransition(w):w()}else I||u(s)},[c,x,s,i,o]),f.current?Vn.createPortal(a.jsx(t,{children:s1&&l?a.jsxs(n1,{mode:c?"visible":"hidden",children:[" ",n]}):n}),p,s):null},(e,t)=>e.active===t.active&&e.renderCount===t.renderCount&&e.children===t.children&&e.exclude===t.exclude&&e.include===t.include),o1=i1,l1=e=>{typeof g.startTransition<"u"&&sl(g.startTransition)?g.startTransition(e):e()},c1=l1;function d1(){return g.useRef(null)}function u1(e){let{activeCacheKey:t,max:r=10,exclude:n,include:s,onBeforeActive:i,customContainerRef:o,cacheNodeClassName:l="cache-component",containerClassName:c="keep-alive-render",errorElement:d,transition:u=!1,viewTransition:h=!1,duration:m=200,children:_,aliveRef:x,maxAliveTime:f=0,enableActivity:p=!1}=e,I=o||g.useRef(null),[S,w]=g.useState([]);g.useLayoutEffect(()=>{Cl(t)||c1(()=>{w(C=>{let F=Date.now();if(C.find(P=>P.cacheKey===t))return C.map(P=>{if(P.cacheKey===t){let V=!1;if(sl(i)&&i(t),f){let X=P.lastActiveTime;if(nl(f)){let _e=f.find(B=>h0(B.match)?B.match.test(t):B.match===t);_e&&(V=_e&&X+_e.expire*1e3r){let P=C.reduce((V,X)=>V.lastActiveTime{w(F=>{let P=C||t;return F.map(V=>V.cacheKey===P?{...V,renderCount:V.renderCount+1}:V)})},[w,t]),y=g.useCallback(C=>{let F=C||t,P=nl(F)?F:[F];return new Promise(V=>{bo(()=>{w(X=>[...X.filter(_e=>!P.includes(_e.cacheKey))]),V()})})},[w,t]),R=g.useCallback(()=>new Promise(C=>{bo(()=>{w([]),C()})}),[w]),j=g.useCallback(C=>{let F=C||t;return new Promise(P=>{bo(()=>{w(V=>[...V.filter(X=>X.cacheKey===F)]),P()})})},[t,w]),T=g.useCallback(()=>S,[S]);return g.useImperativeHandle(x,()=>({refresh:v,destroy:y,destroyAll:R,destroyOther:j,getCacheNodes:T})),a.jsxs(g.Fragment,{children:[a.jsx("div",{ref:I,className:c,style:{height:"100%"}}),S.map(C=>{let{cacheKey:F,ele:P,renderCount:V}=C;return a.jsx(r1,{active:t===F,refresh:v,destroy:y,destroyAll:R,destroyOther:j,getCacheNodes:T,children:a.jsx(o1,{destroy:y,include:s,exclude:n,transition:u,viewTransition:h,duration:m,renderCount:V,containerDivRef:I,errorElement:d,active:t===F,cacheNodeClassName:l,cacheKey:F,enableActivity:p,children:P})},`${F}-${V}`)})]})}var f1=u1;function _0(e,t){return function(){return e.apply(t,arguments)}}const{toString:h1}=Object.prototype,{getPrototypeOf:Al}=Object,{iterator:to,toStringTag:v0}=Symbol,ro=(e=>t=>{const r=h1.call(t);return e[r]||(e[r]=r.slice(8,-1).toLowerCase())})(Object.create(null)),Gr=e=>(e=e.toLowerCase(),t=>ro(t)===e),no=e=>t=>typeof t===e,{isArray:Ps}=Array,Cs=no("undefined");function Aa(e){return e!==null&&!Cs(e)&&e.constructor!==null&&!Cs(e.constructor)&&hr(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const y0=Gr("ArrayBuffer");function m1(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t=ArrayBuffer.isView(e):t=e&&e.buffer&&y0(e.buffer),t}const x1=no("string"),hr=no("function"),w0=no("number"),ka=e=>e!==null&&typeof e=="object",p1=e=>e===!0||e===!1,ui=e=>{if(ro(e)!=="object")return!1;const t=Al(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(v0 in e)&&!(to in e)},g1=e=>{if(!ka(e)||Aa(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},_1=Gr("Date"),v1=Gr("File"),y1=Gr("Blob"),w1=Gr("FileList"),S1=e=>ka(e)&&hr(e.pipe),T1=e=>{let t;return e&&(typeof FormData=="function"&&e instanceof FormData||hr(e.append)&&((t=ro(e))==="formdata"||t==="object"&&hr(e.toString)&&e.toString()==="[object FormData]"))},b1=Gr("URLSearchParams"),[E1,j1,I1,C1]=["ReadableStream","Request","Response","Headers"].map(Gr),A1=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function Fa(e,t,{allOwnKeys:r=!1}={}){if(e===null||typeof e>"u")return;let n,s;if(typeof e!="object"&&(e=[e]),Ps(e))for(n=0,s=e.length;n0;)if(s=r[n],t===s.toLowerCase())return s;return null}const Kn=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,T0=e=>!Cs(e)&&e!==Kn;function il(){const{caseless:e,skipUndefined:t}=T0(this)&&this||{},r={},n=(s,i)=>{const o=e&&S0(r,i)||i;ui(r[o])&&ui(s)?r[o]=il(r[o],s):ui(s)?r[o]=il({},s):Ps(s)?r[o]=s.slice():(!t||!Cs(s))&&(r[o]=s)};for(let s=0,i=arguments.length;s(Fa(t,(s,i)=>{r&&hr(s)?Object.defineProperty(e,i,{value:_0(s,r),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(e,i,{value:s,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:n}),e),F1=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),O1=(e,t,r,n)=>{e.prototype=Object.create(t.prototype,n),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(e,"super",{value:t.prototype}),r&&Object.assign(e.prototype,r)},R1=(e,t,r,n)=>{let s,i,o;const l={};if(t=t||{},e==null)return t;do{for(s=Object.getOwnPropertyNames(e),i=s.length;i-- >0;)o=s[i],(!n||n(o,e,t))&&!l[o]&&(t[o]=e[o],l[o]=!0);e=r!==!1&&Al(e)}while(e&&(!r||r(e,t))&&e!==Object.prototype);return t},D1=(e,t,r)=>{e=String(e),(r===void 0||r>e.length)&&(r=e.length),r-=t.length;const n=e.indexOf(t,r);return n!==-1&&n===r},P1=e=>{if(!e)return null;if(Ps(e))return e;let t=e.length;if(!w0(t))return null;const r=new Array(t);for(;t-- >0;)r[t]=e[t];return r},N1=(e=>t=>e&&t instanceof e)(typeof Uint8Array<"u"&&Al(Uint8Array)),L1=(e,t)=>{const n=(e&&e[to]).call(e);let s;for(;(s=n.next())&&!s.done;){const i=s.value;t.call(e,i[0],i[1])}},M1=(e,t)=>{let r;const n=[];for(;(r=e.exec(t))!==null;)n.push(r);return n},B1=Gr("HTMLFormElement"),U1=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(r,n,s){return n.toUpperCase()+s}),Dc=(({hasOwnProperty:e})=>(t,r)=>e.call(t,r))(Object.prototype),z1=Gr("RegExp"),b0=(e,t)=>{const r=Object.getOwnPropertyDescriptors(e),n={};Fa(r,(s,i)=>{let o;(o=t(s,i,e))!==!1&&(n[i]=o||s)}),Object.defineProperties(e,n)},$1=e=>{b0(e,(t,r)=>{if(hr(e)&&["arguments","caller","callee"].indexOf(r)!==-1)return!1;const n=e[r];if(hr(n)){if(t.enumerable=!1,"writable"in t){t.writable=!1;return}t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+r+"'")})}})},W1=(e,t)=>{const r={},n=s=>{s.forEach(i=>{r[i]=!0})};return Ps(e)?n(e):n(String(e).split(t)),r},H1=()=>{},V1=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function K1(e){return!!(e&&hr(e.append)&&e[v0]==="FormData"&&e[to])}const G1=e=>{const t=new Array(10),r=(n,s)=>{if(ka(n)){if(t.indexOf(n)>=0)return;if(Aa(n))return n;if(!("toJSON"in n)){t[s]=n;const i=Ps(n)?[]:{};return Fa(n,(o,l)=>{const c=r(o,s+1);!Cs(c)&&(i[l]=c)}),t[s]=void 0,i}}return n};return r(e,0)},Y1=Gr("AsyncFunction"),q1=e=>e&&(ka(e)||hr(e))&&hr(e.then)&&hr(e.catch),E0=((e,t)=>e?setImmediate:t?((r,n)=>(Kn.addEventListener("message",({source:s,data:i})=>{s===Kn&&i===r&&n.length&&n.shift()()},!1),s=>{n.push(s),Kn.postMessage(r,"*")}))(`axios@${Math.random()}`,[]):r=>setTimeout(r))(typeof setImmediate=="function",hr(Kn.postMessage)),X1=typeof queueMicrotask<"u"?queueMicrotask.bind(Kn):typeof process<"u"&&process.nextTick||E0,J1=e=>e!=null&&hr(e[to]),ue={isArray:Ps,isArrayBuffer:y0,isBuffer:Aa,isFormData:T1,isArrayBufferView:m1,isString:x1,isNumber:w0,isBoolean:p1,isObject:ka,isPlainObject:ui,isEmptyObject:g1,isReadableStream:E1,isRequest:j1,isResponse:I1,isHeaders:C1,isUndefined:Cs,isDate:_1,isFile:v1,isBlob:y1,isRegExp:z1,isFunction:hr,isStream:S1,isURLSearchParams:b1,isTypedArray:N1,isFileList:w1,forEach:Fa,merge:il,extend:k1,trim:A1,stripBOM:F1,inherits:O1,toFlatObject:R1,kindOf:ro,kindOfTest:Gr,endsWith:D1,toArray:P1,forEachEntry:L1,matchAll:M1,isHTMLForm:B1,hasOwnProperty:Dc,hasOwnProp:Dc,reduceDescriptors:b0,freezeMethods:$1,toObjectSet:W1,toCamelCase:U1,noop:H1,toFiniteNumber:V1,findKey:S0,global:Kn,isContextDefined:T0,isSpecCompliantForm:K1,toJSONObject:G1,isAsyncFn:Y1,isThenable:q1,setImmediate:E0,asap:X1,isIterable:J1};let Ve=class j0 extends Error{static from(t,r,n,s,i,o){const l=new j0(t.message,r||t.code,n,s,i);return l.cause=t,l.name=t.name,o&&Object.assign(l,o),l}constructor(t,r,n,s,i){super(t),this.name="AxiosError",this.isAxiosError=!0,r&&(this.code=r),n&&(this.config=n),s&&(this.request=s),i&&(this.response=i,this.status=i.status)}toJSON(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:ue.toJSONObject(this.config),code:this.code,status:this.status}}};Ve.ERR_BAD_OPTION_VALUE="ERR_BAD_OPTION_VALUE";Ve.ERR_BAD_OPTION="ERR_BAD_OPTION";Ve.ECONNABORTED="ECONNABORTED";Ve.ETIMEDOUT="ETIMEDOUT";Ve.ERR_NETWORK="ERR_NETWORK";Ve.ERR_FR_TOO_MANY_REDIRECTS="ERR_FR_TOO_MANY_REDIRECTS";Ve.ERR_DEPRECATED="ERR_DEPRECATED";Ve.ERR_BAD_RESPONSE="ERR_BAD_RESPONSE";Ve.ERR_BAD_REQUEST="ERR_BAD_REQUEST";Ve.ERR_CANCELED="ERR_CANCELED";Ve.ERR_NOT_SUPPORT="ERR_NOT_SUPPORT";Ve.ERR_INVALID_URL="ERR_INVALID_URL";const Z1=null;function ol(e){return ue.isPlainObject(e)||ue.isArray(e)}function I0(e){return ue.endsWith(e,"[]")?e.slice(0,-2):e}function Pc(e,t,r){return e?e.concat(t).map(function(s,i){return s=I0(s),!r&&i?"["+s+"]":s}).join(r?".":""):t}function Q1(e){return ue.isArray(e)&&!e.some(ol)}const eg=ue.toFlatObject(ue,{},null,function(t){return/^is[A-Z]/.test(t)});function so(e,t,r){if(!ue.isObject(e))throw new TypeError("target must be an object");t=t||new FormData,r=ue.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,function(f,p){return!ue.isUndefined(p[f])});const n=r.metaTokens,s=r.visitor||u,i=r.dots,o=r.indexes,c=(r.Blob||typeof Blob<"u"&&Blob)&&ue.isSpecCompliantForm(t);if(!ue.isFunction(s))throw new TypeError("visitor must be a function");function d(x){if(x===null)return"";if(ue.isDate(x))return x.toISOString();if(ue.isBoolean(x))return x.toString();if(!c&&ue.isBlob(x))throw new Ve("Blob is not supported. Use a Buffer instead.");return ue.isArrayBuffer(x)||ue.isTypedArray(x)?c&&typeof Blob=="function"?new Blob([x]):Buffer.from(x):x}function u(x,f,p){let I=x;if(x&&!p&&typeof x=="object"){if(ue.endsWith(f,"{}"))f=n?f:f.slice(0,-2),x=JSON.stringify(x);else if(ue.isArray(x)&&Q1(x)||(ue.isFileList(x)||ue.endsWith(f,"[]"))&&(I=ue.toArray(x)))return f=I0(f),I.forEach(function(w,v){!(ue.isUndefined(w)||w===null)&&t.append(o===!0?Pc([f],v,i):o===null?f:f+"[]",d(w))}),!1}return ol(x)?!0:(t.append(Pc(p,f,i),d(x)),!1)}const h=[],m=Object.assign(eg,{defaultVisitor:u,convertValue:d,isVisitable:ol});function _(x,f){if(!ue.isUndefined(x)){if(h.indexOf(x)!==-1)throw Error("Circular reference detected in "+f.join("."));h.push(x),ue.forEach(x,function(I,S){(!(ue.isUndefined(I)||I===null)&&s.call(t,I,ue.isString(S)?S.trim():S,f,m))===!0&&_(I,f?f.concat(S):[S])}),h.pop()}}if(!ue.isObject(e))throw new TypeError("data must be an object");return _(e),t}function Nc(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(n){return t[n]})}function kl(e,t){this._pairs=[],e&&so(e,this,t)}const C0=kl.prototype;C0.append=function(t,r){this._pairs.push([t,r])};C0.toString=function(t){const r=t?function(n){return t.call(this,n,Nc)}:Nc;return this._pairs.map(function(s){return r(s[0])+"="+r(s[1])},"").join("&")};function tg(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function A0(e,t,r){if(!t)return e;const n=r&&r.encode||tg,s=ue.isFunction(r)?{serialize:r}:r,i=s&&s.serialize;let o;if(i?o=i(t,s):o=ue.isURLSearchParams(t)?t.toString():new kl(t,s).toString(n),o){const l=e.indexOf("#");l!==-1&&(e=e.slice(0,l)),e+=(e.indexOf("?")===-1?"?":"&")+o}return e}class Lc{constructor(){this.handlers=[]}use(t,r,n){return this.handlers.push({fulfilled:t,rejected:r,synchronous:n?n.synchronous:!1,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(t){ue.forEach(this.handlers,function(n){n!==null&&t(n)})}}const k0={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},rg=typeof URLSearchParams<"u"?URLSearchParams:kl,ng=typeof FormData<"u"?FormData:null,sg=typeof Blob<"u"?Blob:null,ag={isBrowser:!0,classes:{URLSearchParams:rg,FormData:ng,Blob:sg},protocols:["http","https","file","blob","url","data"]},Fl=typeof window<"u"&&typeof document<"u",ll=typeof navigator=="object"&&navigator||void 0,ig=Fl&&(!ll||["ReactNative","NativeScript","NS"].indexOf(ll.product)<0),og=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",lg=Fl&&window.location.href||"http://localhost",cg=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:Fl,hasStandardBrowserEnv:ig,hasStandardBrowserWebWorkerEnv:og,navigator:ll,origin:lg},Symbol.toStringTag,{value:"Module"})),nr={...cg,...ag};function dg(e,t){return so(e,new nr.classes.URLSearchParams,{visitor:function(r,n,s,i){return nr.isNode&&ue.isBuffer(r)?(this.append(n,r.toString("base64")),!1):i.defaultVisitor.apply(this,arguments)},...t})}function ug(e){return ue.matchAll(/\w+|\[(\w*)]/g,e).map(t=>t[0]==="[]"?"":t[1]||t[0])}function fg(e){const t={},r=Object.keys(e);let n;const s=r.length;let i;for(n=0;n=r.length;return o=!o&&ue.isArray(s)?s.length:o,c?(ue.hasOwnProp(s,o)?s[o]=[s[o],n]:s[o]=n,!l):((!s[o]||!ue.isObject(s[o]))&&(s[o]=[]),t(r,n,s[o],i)&&ue.isArray(s[o])&&(s[o]=fg(s[o])),!l)}if(ue.isFormData(e)&&ue.isFunction(e.entries)){const r={};return ue.forEachEntry(e,(n,s)=>{t(ug(n),s,r,0)}),r}return null}function hg(e,t,r){if(ue.isString(e))try{return(t||JSON.parse)(e),ue.trim(e)}catch(n){if(n.name!=="SyntaxError")throw n}return(r||JSON.stringify)(e)}const Oa={transitional:k0,adapter:["xhr","http","fetch"],transformRequest:[function(t,r){const n=r.getContentType()||"",s=n.indexOf("application/json")>-1,i=ue.isObject(t);if(i&&ue.isHTMLForm(t)&&(t=new FormData(t)),ue.isFormData(t))return s?JSON.stringify(F0(t)):t;if(ue.isArrayBuffer(t)||ue.isBuffer(t)||ue.isStream(t)||ue.isFile(t)||ue.isBlob(t)||ue.isReadableStream(t))return t;if(ue.isArrayBufferView(t))return t.buffer;if(ue.isURLSearchParams(t))return r.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),t.toString();let l;if(i){if(n.indexOf("application/x-www-form-urlencoded")>-1)return dg(t,this.formSerializer).toString();if((l=ue.isFileList(t))||n.indexOf("multipart/form-data")>-1){const c=this.env&&this.env.FormData;return so(l?{"files[]":t}:t,c&&new c,this.formSerializer)}}return i||s?(r.setContentType("application/json",!1),hg(t)):t}],transformResponse:[function(t){const r=this.transitional||Oa.transitional,n=r&&r.forcedJSONParsing,s=this.responseType==="json";if(ue.isResponse(t)||ue.isReadableStream(t))return t;if(t&&ue.isString(t)&&(n&&!this.responseType||s)){const o=!(r&&r.silentJSONParsing)&&s;try{return JSON.parse(t,this.parseReviver)}catch(l){if(o)throw l.name==="SyntaxError"?Ve.from(l,Ve.ERR_BAD_RESPONSE,this,null,this.response):l}}return t}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:nr.classes.FormData,Blob:nr.classes.Blob},validateStatus:function(t){return t>=200&&t<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};ue.forEach(["delete","get","head","post","put","patch"],e=>{Oa.headers[e]={}});const mg=ue.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),xg=e=>{const t={};let r,n,s;return e&&e.split(` +`).forEach(function(o){s=o.indexOf(":"),r=o.substring(0,s).trim().toLowerCase(),n=o.substring(s+1).trim(),!(!r||t[r]&&mg[r])&&(r==="set-cookie"?t[r]?t[r].push(n):t[r]=[n]:t[r]=t[r]?t[r]+", "+n:n)}),t},Mc=Symbol("internals");function Us(e){return e&&String(e).trim().toLowerCase()}function fi(e){return e===!1||e==null?e:ue.isArray(e)?e.map(fi):String(e)}function pg(e){const t=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let n;for(;n=r.exec(e);)t[n[1]]=n[2];return t}const gg=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function Eo(e,t,r,n,s){if(ue.isFunction(n))return n.call(this,t,r);if(s&&(t=r),!!ue.isString(t)){if(ue.isString(n))return t.indexOf(n)!==-1;if(ue.isRegExp(n))return n.test(t)}}function _g(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,r,n)=>r.toUpperCase()+n)}function vg(e,t){const r=ue.toCamelCase(" "+t);["get","set","has"].forEach(n=>{Object.defineProperty(e,n+r,{value:function(s,i,o){return this[n].call(this,t,s,i,o)},configurable:!0})})}let mr=class{constructor(t){t&&this.set(t)}set(t,r,n){const s=this;function i(l,c,d){const u=Us(c);if(!u)throw new Error("header name must be a non-empty string");const h=ue.findKey(s,u);(!h||s[h]===void 0||d===!0||d===void 0&&s[h]!==!1)&&(s[h||c]=fi(l))}const o=(l,c)=>ue.forEach(l,(d,u)=>i(d,u,c));if(ue.isPlainObject(t)||t instanceof this.constructor)o(t,r);else if(ue.isString(t)&&(t=t.trim())&&!gg(t))o(xg(t),r);else if(ue.isObject(t)&&ue.isIterable(t)){let l={},c,d;for(const u of t){if(!ue.isArray(u))throw TypeError("Object iterator must return a key-value pair");l[d=u[0]]=(c=l[d])?ue.isArray(c)?[...c,u[1]]:[c,u[1]]:u[1]}o(l,r)}else t!=null&&i(r,t,n);return this}get(t,r){if(t=Us(t),t){const n=ue.findKey(this,t);if(n){const s=this[n];if(!r)return s;if(r===!0)return pg(s);if(ue.isFunction(r))return r.call(this,s,n);if(ue.isRegExp(r))return r.exec(s);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,r){if(t=Us(t),t){const n=ue.findKey(this,t);return!!(n&&this[n]!==void 0&&(!r||Eo(this,this[n],n,r)))}return!1}delete(t,r){const n=this;let s=!1;function i(o){if(o=Us(o),o){const l=ue.findKey(n,o);l&&(!r||Eo(n,n[l],l,r))&&(delete n[l],s=!0)}}return ue.isArray(t)?t.forEach(i):i(t),s}clear(t){const r=Object.keys(this);let n=r.length,s=!1;for(;n--;){const i=r[n];(!t||Eo(this,this[i],i,t,!0))&&(delete this[i],s=!0)}return s}normalize(t){const r=this,n={};return ue.forEach(this,(s,i)=>{const o=ue.findKey(n,i);if(o){r[o]=fi(s),delete r[i];return}const l=t?_g(i):String(i).trim();l!==i&&delete r[i],r[l]=fi(s),n[l]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const r=Object.create(null);return ue.forEach(this,(n,s)=>{n!=null&&n!==!1&&(r[s]=t&&ue.isArray(n)?n.join(", "):n)}),r}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,r])=>t+": "+r).join(` +`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...r){const n=new this(t);return r.forEach(s=>n.set(s)),n}static accessor(t){const n=(this[Mc]=this[Mc]={accessors:{}}).accessors,s=this.prototype;function i(o){const l=Us(o);n[l]||(vg(s,o),n[l]=!0)}return ue.isArray(t)?t.forEach(i):i(t),this}};mr.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);ue.reduceDescriptors(mr.prototype,({value:e},t)=>{let r=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(n){this[r]=n}}});ue.freezeMethods(mr);function jo(e,t){const r=this||Oa,n=t||r,s=mr.from(n.headers);let i=n.data;return ue.forEach(e,function(l){i=l.call(r,i,s.normalize(),t?t.status:void 0)}),s.normalize(),i}function O0(e){return!!(e&&e.__CANCEL__)}let Ra=class extends Ve{constructor(t,r,n){super(t??"canceled",Ve.ERR_CANCELED,r,n),this.name="CanceledError",this.__CANCEL__=!0}};function R0(e,t,r){const n=r.config.validateStatus;!r.status||!n||n(r.status)?e(r):t(new Ve("Request failed with status code "+r.status,[Ve.ERR_BAD_REQUEST,Ve.ERR_BAD_RESPONSE][Math.floor(r.status/100)-4],r.config,r.request,r))}function yg(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}function wg(e,t){e=e||10;const r=new Array(e),n=new Array(e);let s=0,i=0,o;return t=t!==void 0?t:1e3,function(c){const d=Date.now(),u=n[i];o||(o=d),r[s]=c,n[s]=d;let h=i,m=0;for(;h!==s;)m+=r[h++],h=h%e;if(s=(s+1)%e,s===i&&(i=(i+1)%e),d-o{r=u,s=null,i&&(clearTimeout(i),i=null),e(...d)};return[(...d)=>{const u=Date.now(),h=u-r;h>=n?o(d,u):(s=d,i||(i=setTimeout(()=>{i=null,o(s)},n-h)))},()=>s&&o(s)]}const Ti=(e,t,r=3)=>{let n=0;const s=wg(50,250);return Sg(i=>{const o=i.loaded,l=i.lengthComputable?i.total:void 0,c=o-n,d=s(c),u=o<=l;n=o;const h={loaded:o,total:l,progress:l?o/l:void 0,bytes:c,rate:d||void 0,estimated:d&&l&&u?(l-o)/d:void 0,event:i,lengthComputable:l!=null,[t?"download":"upload"]:!0};e(h)},r)},Bc=(e,t)=>{const r=e!=null;return[n=>t[0]({lengthComputable:r,total:e,loaded:n}),t[1]]},Uc=e=>(...t)=>ue.asap(()=>e(...t)),Tg=nr.hasStandardBrowserEnv?((e,t)=>r=>(r=new URL(r,nr.origin),e.protocol===r.protocol&&e.host===r.host&&(t||e.port===r.port)))(new URL(nr.origin),nr.navigator&&/(msie|trident)/i.test(nr.navigator.userAgent)):()=>!0,bg=nr.hasStandardBrowserEnv?{write(e,t,r,n,s,i,o){if(typeof document>"u")return;const l=[`${e}=${encodeURIComponent(t)}`];ue.isNumber(r)&&l.push(`expires=${new Date(r).toUTCString()}`),ue.isString(n)&&l.push(`path=${n}`),ue.isString(s)&&l.push(`domain=${s}`),i===!0&&l.push("secure"),ue.isString(o)&&l.push(`SameSite=${o}`),document.cookie=l.join("; ")},read(e){if(typeof document>"u")return null;const t=document.cookie.match(new RegExp("(?:^|; )"+e+"=([^;]*)"));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,"",Date.now()-864e5,"/")}}:{write(){},read(){return null},remove(){}};function Eg(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function jg(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function D0(e,t,r){let n=!Eg(t);return e&&(n||r==!1)?jg(e,t):t}const zc=e=>e instanceof mr?{...e}:e;function qn(e,t){t=t||{};const r={};function n(d,u,h,m){return ue.isPlainObject(d)&&ue.isPlainObject(u)?ue.merge.call({caseless:m},d,u):ue.isPlainObject(u)?ue.merge({},u):ue.isArray(u)?u.slice():u}function s(d,u,h,m){if(ue.isUndefined(u)){if(!ue.isUndefined(d))return n(void 0,d,h,m)}else return n(d,u,h,m)}function i(d,u){if(!ue.isUndefined(u))return n(void 0,u)}function o(d,u){if(ue.isUndefined(u)){if(!ue.isUndefined(d))return n(void 0,d)}else return n(void 0,u)}function l(d,u,h){if(h in t)return n(d,u);if(h in e)return n(void 0,d)}const c={url:i,method:i,data:i,baseURL:o,transformRequest:o,transformResponse:o,paramsSerializer:o,timeout:o,timeoutMessage:o,withCredentials:o,withXSRFToken:o,adapter:o,responseType:o,xsrfCookieName:o,xsrfHeaderName:o,onUploadProgress:o,onDownloadProgress:o,decompress:o,maxContentLength:o,maxBodyLength:o,beforeRedirect:o,transport:o,httpAgent:o,httpsAgent:o,cancelToken:o,socketPath:o,responseEncoding:o,validateStatus:l,headers:(d,u,h)=>s(zc(d),zc(u),h,!0)};return ue.forEach(Object.keys({...e,...t}),function(u){const h=c[u]||s,m=h(e[u],t[u],u);ue.isUndefined(m)&&h!==l||(r[u]=m)}),r}const P0=e=>{const t=qn({},e);let{data:r,withXSRFToken:n,xsrfHeaderName:s,xsrfCookieName:i,headers:o,auth:l}=t;if(t.headers=o=mr.from(o),t.url=A0(D0(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),l&&o.set("Authorization","Basic "+btoa((l.username||"")+":"+(l.password?unescape(encodeURIComponent(l.password)):""))),ue.isFormData(r)){if(nr.hasStandardBrowserEnv||nr.hasStandardBrowserWebWorkerEnv)o.setContentType(void 0);else if(ue.isFunction(r.getHeaders)){const c=r.getHeaders(),d=["content-type","content-length"];Object.entries(c).forEach(([u,h])=>{d.includes(u.toLowerCase())&&o.set(u,h)})}}if(nr.hasStandardBrowserEnv&&(n&&ue.isFunction(n)&&(n=n(t)),n||n!==!1&&Tg(t.url))){const c=s&&i&&bg.read(i);c&&o.set(s,c)}return t},Ig=typeof XMLHttpRequest<"u",Cg=Ig&&function(e){return new Promise(function(r,n){const s=P0(e);let i=s.data;const o=mr.from(s.headers).normalize();let{responseType:l,onUploadProgress:c,onDownloadProgress:d}=s,u,h,m,_,x;function f(){_&&_(),x&&x(),s.cancelToken&&s.cancelToken.unsubscribe(u),s.signal&&s.signal.removeEventListener("abort",u)}let p=new XMLHttpRequest;p.open(s.method.toUpperCase(),s.url,!0),p.timeout=s.timeout;function I(){if(!p)return;const w=mr.from("getAllResponseHeaders"in p&&p.getAllResponseHeaders()),y={data:!l||l==="text"||l==="json"?p.responseText:p.response,status:p.status,statusText:p.statusText,headers:w,config:e,request:p};R0(function(j){r(j),f()},function(j){n(j),f()},y),p=null}"onloadend"in p?p.onloadend=I:p.onreadystatechange=function(){!p||p.readyState!==4||p.status===0&&!(p.responseURL&&p.responseURL.indexOf("file:")===0)||setTimeout(I)},p.onabort=function(){p&&(n(new Ve("Request aborted",Ve.ECONNABORTED,e,p)),p=null)},p.onerror=function(v){const y=v&&v.message?v.message:"Network Error",R=new Ve(y,Ve.ERR_NETWORK,e,p);R.event=v||null,n(R),p=null},p.ontimeout=function(){let v=s.timeout?"timeout of "+s.timeout+"ms exceeded":"timeout exceeded";const y=s.transitional||k0;s.timeoutErrorMessage&&(v=s.timeoutErrorMessage),n(new Ve(v,y.clarifyTimeoutError?Ve.ETIMEDOUT:Ve.ECONNABORTED,e,p)),p=null},i===void 0&&o.setContentType(null),"setRequestHeader"in p&&ue.forEach(o.toJSON(),function(v,y){p.setRequestHeader(y,v)}),ue.isUndefined(s.withCredentials)||(p.withCredentials=!!s.withCredentials),l&&l!=="json"&&(p.responseType=s.responseType),d&&([m,x]=Ti(d,!0),p.addEventListener("progress",m)),c&&p.upload&&([h,_]=Ti(c),p.upload.addEventListener("progress",h),p.upload.addEventListener("loadend",_)),(s.cancelToken||s.signal)&&(u=w=>{p&&(n(!w||w.type?new Ra(null,e,p):w),p.abort(),p=null)},s.cancelToken&&s.cancelToken.subscribe(u),s.signal&&(s.signal.aborted?u():s.signal.addEventListener("abort",u)));const S=yg(s.url);if(S&&nr.protocols.indexOf(S)===-1){n(new Ve("Unsupported protocol "+S+":",Ve.ERR_BAD_REQUEST,e));return}p.send(i||null)})},Ag=(e,t)=>{const{length:r}=e=e?e.filter(Boolean):[];if(t||r){let n=new AbortController,s;const i=function(d){if(!s){s=!0,l();const u=d instanceof Error?d:this.reason;n.abort(u instanceof Ve?u:new Ra(u instanceof Error?u.message:u))}};let o=t&&setTimeout(()=>{o=null,i(new Ve(`timeout of ${t}ms exceeded`,Ve.ETIMEDOUT))},t);const l=()=>{e&&(o&&clearTimeout(o),o=null,e.forEach(d=>{d.unsubscribe?d.unsubscribe(i):d.removeEventListener("abort",i)}),e=null)};e.forEach(d=>d.addEventListener("abort",i));const{signal:c}=n;return c.unsubscribe=()=>ue.asap(l),c}},kg=function*(e,t){let r=e.byteLength;if(r{const s=Fg(e,t);let i=0,o,l=c=>{o||(o=!0,n&&n(c))};return new ReadableStream({async pull(c){try{const{done:d,value:u}=await s.next();if(d){l(),c.close();return}let h=u.byteLength;if(r){let m=i+=h;r(m)}c.enqueue(new Uint8Array(u))}catch(d){throw l(d),d}},cancel(c){return l(c),s.return()}},{highWaterMark:2})},Wc=64*1024,{isFunction:$a}=ue,Rg=(({Request:e,Response:t})=>({Request:e,Response:t}))(ue.global),{ReadableStream:Hc,TextEncoder:Vc}=ue.global,Kc=(e,...t)=>{try{return!!e(...t)}catch{return!1}},Dg=e=>{e=ue.merge.call({skipUndefined:!0},Rg,e);const{fetch:t,Request:r,Response:n}=e,s=t?$a(t):typeof fetch=="function",i=$a(r),o=$a(n);if(!s)return!1;const l=s&&$a(Hc),c=s&&(typeof Vc=="function"?(x=>f=>x.encode(f))(new Vc):async x=>new Uint8Array(await new r(x).arrayBuffer())),d=i&&l&&Kc(()=>{let x=!1;const f=new r(nr.origin,{body:new Hc,method:"POST",get duplex(){return x=!0,"half"}}).headers.has("Content-Type");return x&&!f}),u=o&&l&&Kc(()=>ue.isReadableStream(new n("").body)),h={stream:u&&(x=>x.body)};s&&["text","arrayBuffer","blob","formData","stream"].forEach(x=>{!h[x]&&(h[x]=(f,p)=>{let I=f&&f[x];if(I)return I.call(f);throw new Ve(`Response type '${x}' is not supported`,Ve.ERR_NOT_SUPPORT,p)})});const m=async x=>{if(x==null)return 0;if(ue.isBlob(x))return x.size;if(ue.isSpecCompliantForm(x))return(await new r(nr.origin,{method:"POST",body:x}).arrayBuffer()).byteLength;if(ue.isArrayBufferView(x)||ue.isArrayBuffer(x))return x.byteLength;if(ue.isURLSearchParams(x)&&(x=x+""),ue.isString(x))return(await c(x)).byteLength},_=async(x,f)=>{const p=ue.toFiniteNumber(x.getContentLength());return p??m(f)};return async x=>{let{url:f,method:p,data:I,signal:S,cancelToken:w,timeout:v,onDownloadProgress:y,onUploadProgress:R,responseType:j,headers:T,withCredentials:C="same-origin",fetchOptions:F}=P0(x),P=t||fetch;j=j?(j+"").toLowerCase():"text";let V=Ag([S,w&&w.toAbortSignal()],v),X=null;const _e=V&&V.unsubscribe&&(()=>{V.unsubscribe()});let B;try{if(R&&d&&p!=="get"&&p!=="head"&&(B=await _(T,I))!==0){let de=new r(f,{method:"POST",body:I,duplex:"half"}),k;if(ue.isFormData(I)&&(k=de.headers.get("content-type"))&&T.setContentType(k),de.body){const[W,z]=Bc(B,Ti(Uc(R)));I=$c(de.body,Wc,W,z)}}ue.isString(C)||(C=C?"include":"omit");const te=i&&"credentials"in r.prototype,J={...F,signal:V,method:p.toUpperCase(),headers:T.normalize().toJSON(),body:I,duplex:"half",credentials:te?C:void 0};X=i&&new r(f,J);let G=await(i?P(X,F):P(f,J));const se=u&&(j==="stream"||j==="response");if(u&&(y||se&&_e)){const de={};["status","statusText","headers"].forEach(D=>{de[D]=G[D]});const k=ue.toFiniteNumber(G.headers.get("content-length")),[W,z]=y&&Bc(k,Ti(Uc(y),!0))||[];G=new n($c(G.body,Wc,W,()=>{z&&z(),_e&&_e()}),de)}j=j||"text";let Q=await h[ue.findKey(h,j)||"text"](G,x);return!se&&_e&&_e(),await new Promise((de,k)=>{R0(de,k,{data:Q,headers:mr.from(G.headers),status:G.status,statusText:G.statusText,config:x,request:X})})}catch(te){throw _e&&_e(),te&&te.name==="TypeError"&&/Load failed|fetch/i.test(te.message)?Object.assign(new Ve("Network Error",Ve.ERR_NETWORK,x,X),{cause:te.cause||te}):Ve.from(te,te&&te.code,x,X)}}},Pg=new Map,N0=e=>{let t=e&&e.env||{};const{fetch:r,Request:n,Response:s}=t,i=[n,s,r];let o=i.length,l=o,c,d,u=Pg;for(;l--;)c=i[l],d=u.get(c),d===void 0&&u.set(c,d=l?new Map:Dg(t)),u=d;return d};N0();const Ol={http:Z1,xhr:Cg,fetch:{get:N0}};ue.forEach(Ol,(e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch{}Object.defineProperty(e,"adapterName",{value:t})}});const Gc=e=>`- ${e}`,Ng=e=>ue.isFunction(e)||e===null||e===!1;function Lg(e,t){e=ue.isArray(e)?e:[e];const{length:r}=e;let n,s;const i={};for(let o=0;o`adapter ${c} `+(d===!1?"is not supported by the environment":"is not available in the build"));let l=r?o.length>1?`since : +`+o.map(Gc).join(` +`):" "+Gc(o[0]):"as no adapter specified";throw new Ve("There is no suitable adapter to dispatch the request "+l,"ERR_NOT_SUPPORT")}return s}const L0={getAdapter:Lg,adapters:Ol};function Io(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Ra(null,e)}function Yc(e){return Io(e),e.headers=mr.from(e.headers),e.data=jo.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),L0.getAdapter(e.adapter||Oa.adapter,e)(e).then(function(n){return Io(e),n.data=jo.call(e,e.transformResponse,n),n.headers=mr.from(n.headers),n},function(n){return O0(n)||(Io(e),n&&n.response&&(n.response.data=jo.call(e,e.transformResponse,n.response),n.response.headers=mr.from(n.response.headers))),Promise.reject(n)})}const M0="1.13.3",ao={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{ao[e]=function(n){return typeof n===e||"a"+(t<1?"n ":" ")+e}});const qc={};ao.transitional=function(t,r,n){function s(i,o){return"[Axios v"+M0+"] Transitional option '"+i+"'"+o+(n?". "+n:"")}return(i,o,l)=>{if(t===!1)throw new Ve(s(o," has been removed"+(r?" in "+r:"")),Ve.ERR_DEPRECATED);return r&&!qc[o]&&(qc[o]=!0,console.warn(s(o," has been deprecated since v"+r+" and will be removed in the near future"))),t?t(i,o,l):!0}};ao.spelling=function(t){return(r,n)=>(console.warn(`${n} is likely a misspelling of ${t}`),!0)};function Mg(e,t,r){if(typeof e!="object")throw new Ve("options must be an object",Ve.ERR_BAD_OPTION_VALUE);const n=Object.keys(e);let s=n.length;for(;s-- >0;){const i=n[s],o=t[i];if(o){const l=e[i],c=l===void 0||o(l,i,e);if(c!==!0)throw new Ve("option "+i+" must be "+c,Ve.ERR_BAD_OPTION_VALUE);continue}if(r!==!0)throw new Ve("Unknown option "+i,Ve.ERR_BAD_OPTION)}}const hi={assertOptions:Mg,validators:ao},Jr=hi.validators;let Gn=class{constructor(t){this.defaults=t||{},this.interceptors={request:new Lc,response:new Lc}}async request(t,r){try{return await this._request(t,r)}catch(n){if(n instanceof Error){let s={};Error.captureStackTrace?Error.captureStackTrace(s):s=new Error;const i=s.stack?s.stack.replace(/^.+\n/,""):"";try{n.stack?i&&!String(n.stack).endsWith(i.replace(/^.+\n.+\n/,""))&&(n.stack+=` +`+i):n.stack=i}catch{}}throw n}}_request(t,r){typeof t=="string"?(r=r||{},r.url=t):r=t||{},r=qn(this.defaults,r);const{transitional:n,paramsSerializer:s,headers:i}=r;n!==void 0&&hi.assertOptions(n,{silentJSONParsing:Jr.transitional(Jr.boolean),forcedJSONParsing:Jr.transitional(Jr.boolean),clarifyTimeoutError:Jr.transitional(Jr.boolean)},!1),s!=null&&(ue.isFunction(s)?r.paramsSerializer={serialize:s}:hi.assertOptions(s,{encode:Jr.function,serialize:Jr.function},!0)),r.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?r.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:r.allowAbsoluteUrls=!0),hi.assertOptions(r,{baseUrl:Jr.spelling("baseURL"),withXsrfToken:Jr.spelling("withXSRFToken")},!0),r.method=(r.method||this.defaults.method||"get").toLowerCase();let o=i&&ue.merge(i.common,i[r.method]);i&&ue.forEach(["delete","get","head","post","put","patch","common"],x=>{delete i[x]}),r.headers=mr.concat(o,i);const l=[];let c=!0;this.interceptors.request.forEach(function(f){typeof f.runWhen=="function"&&f.runWhen(r)===!1||(c=c&&f.synchronous,l.unshift(f.fulfilled,f.rejected))});const d=[];this.interceptors.response.forEach(function(f){d.push(f.fulfilled,f.rejected)});let u,h=0,m;if(!c){const x=[Yc.bind(this),void 0];x.unshift(...l),x.push(...d),m=x.length,u=Promise.resolve(r);let f=r;for(;h{f=p!==void 0?p:f}).catch(x[h++]).then(()=>f);return u}m=l.length;let _=r;for(;h{if(!n._listeners)return;let i=n._listeners.length;for(;i-- >0;)n._listeners[i](s);n._listeners=null}),this.promise.then=s=>{let i;const o=new Promise(l=>{n.subscribe(l),i=l}).then(s);return o.cancel=function(){n.unsubscribe(i)},o},t(function(i,o,l){n.reason||(n.reason=new Ra(i,o,l),r(n.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const r=this._listeners.indexOf(t);r!==-1&&this._listeners.splice(r,1)}toAbortSignal(){const t=new AbortController,r=n=>{t.abort(n)};return this.subscribe(r),t.signal.unsubscribe=()=>this.unsubscribe(r),t.signal}static source(){let t;return{token:new B0(function(s){t=s}),cancel:t}}};function Ug(e){return function(r){return e.apply(null,r)}}function zg(e){return ue.isObject(e)&&e.isAxiosError===!0}const cl={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(cl).forEach(([e,t])=>{cl[t]=e});function U0(e){const t=new Gn(e),r=_0(Gn.prototype.request,t);return ue.extend(r,Gn.prototype,t,{allOwnKeys:!0}),ue.extend(r,t,null,{allOwnKeys:!0}),r.create=function(s){return U0(qn(e,s))},r}const At=U0(Oa);At.Axios=Gn;At.CanceledError=Ra;At.CancelToken=Bg;At.isCancel=O0;At.VERSION=M0;At.toFormData=so;At.AxiosError=Ve;At.Cancel=At.CanceledError;At.all=function(t){return Promise.all(t)};At.spread=Ug;At.isAxiosError=zg;At.mergeConfig=qn;At.AxiosHeaders=mr;At.formToJSON=e=>F0(ue.isHTMLForm(e)?new FormData(e):e);At.getAdapter=L0.getAdapter;At.HttpStatusCode=cl;At.default=At;const{Axios:u4,AxiosError:f4,CanceledError:h4,isCancel:m4,CancelToken:x4,VERSION:p4,all:g4,Cancel:_4,isAxiosError:v4,spread:y4,toFormData:w4,AxiosHeaders:S4,HttpStatusCode:T4,formToJSON:b4,getAdapter:E4,mergeConfig:j4}=At;/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */function $g(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array"}function fr(e,t,r=""){const n=$g(e),s=e==null?void 0:e.length,i=t!==void 0;if(!n||i&&s!==t){const o=r&&`"${r}" `,l=i?` of length ${t}`:"",c=n?`length=${s}`:`type=${typeof e}`;throw new Error(o+"expected Uint8Array"+l+", got "+c)}return e}function bi(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function z0(e,t){fr(e,void 0,"output");const r=t.outputLen;if(e.length{function r(n,...s){if(fr(n,void 0,"key"),!Hg)throw new Error("Non little-endian hardware is not yet supported");if(e.nonceLength!==void 0){const u=s[0];fr(u,e.varSizeNonce?void 0:e.nonceLength,"nonce")}const i=e.tagLength;i&&s[1]!==void 0&&fr(s[1],void 0,"AAD");const o=t(n,...s),l=(u,h)=>{if(h!==void 0){if(u!==2)throw new Error("cipher output not supported");fr(h,void 0,"output")}};let c=!1;return{encrypt(u,h){if(c)throw new Error("cannot encrypt() twice with same key + nonce");return c=!0,fr(u),l(o.encrypt.length,h),o.encrypt(u,h)},decrypt(u,h){if(fr(u),i&&u.length{const s=n&1;return{s3:r<<31|n>>>1,s2:t<<31|r>>>1,s1:e<<31|t>>>1,s0:e>>>1^qg<<24&-(s&1)}},Pr=e=>(e>>>0&255)<<24|(e>>>8&255)<<16|(e>>>16&255)<<8|e>>>24&255|0;function Jg(e){e.reverse();const t=e[15]&1;let r=0;for(let n=0;n>>1|r,r=(s&1)<<7}return e[0]^=-t&225,e}const Zg=e=>e>64*1024?8:e>1024?4:2;class $0{constructor(t,r){Or(this,"blockLen",fn);Or(this,"outputLen",fn);Or(this,"s0",0);Or(this,"s1",0);Or(this,"s2",0);Or(this,"s3",0);Or(this,"finished",!1);Or(this,"t");Or(this,"W");Or(this,"windowSize");fr(t,16,"key"),t=Xn(t);const n=io(t);let s=n.getUint32(0,!1),i=n.getUint32(4,!1),o=n.getUint32(8,!1),l=n.getUint32(12,!1);const c=[];for(let x=0;x<128;x++)c.push({s0:Pr(s),s1:Pr(i),s2:Pr(o),s3:Pr(l)}),{s0:s,s1:i,s2:o,s3:l}=Xg(s,i,o,l);const d=Zg(r||1024);if(![1,2,4,8].includes(d))throw new Error("ghash: invalid window size, expected 2, 4 or 8");this.W=d;const h=128/d,m=this.windowSize=2**d,_=[];for(let x=0;x>>d-v-1&1))continue;const{s0:R,s1:j,s2:T,s3:C}=c[d*x+v];p^=R,I^=j,S^=T,w^=C}_.push({s0:p,s1:I,s2:S,s3:w})}this.t=_}_updateBlock(t,r,n,s){t^=this.s0,r^=this.s1,n^=this.s2,s^=this.s3;const{W:i,t:o,windowSize:l}=this;let c=0,d=0,u=0,h=0;const m=(1<>>8*f&255;for(let I=8/i-1;I>=0;I--){const S=p>>>i*I&m,{s0:w,s1:v,s2:y,s3:R}=o[_*l+S];c^=w,d^=v,u^=y,h^=R,_+=1}}this.s0=c,this.s1=d,this.s2=u,this.s3=h}update(t){bi(this),fr(t),t=Xn(t);const r=mn(t),n=Math.floor(t.length/fn),s=t.length%fn;for(let i=0;ie(s,n.length).update(n).digest(),r=e(new Uint8Array(16),0);return t.outputLen=r.outputLen,t.blockLen=r.blockLen,t.create=(n,s)=>e(n,s),t}const Xc=W0((e,t)=>new $0(e,t));W0((e,t)=>new Qg(e,t));const dl=16,e_=4,Wa=new Uint8Array(dl),t_=283;function r_(e){if(![16,24,32].includes(e.length))throw new Error('"aes key" expected Uint8Array of length 16/24/32, got length='+e.length)}function Dl(e){return e<<1^t_&-(e>>7)}function Jc(e,t){let r=0;for(;t>0;t>>=1)r^=e&-(t&1),e=Dl(e);return r}const n_=(()=>{const e=new Uint8Array(256);for(let r=0,n=1;r<256;r++,n^=Dl(n))e[r]=n;const t=new Uint8Array(256);t[0]=99;for(let r=0;r<255;r++){let n=e[255-r];n|=n<<8,t[e[r]]=(n^n>>4^n>>5^n>>6^n>>7^99)&255}return _n(e),t})(),s_=e=>e<<24|e>>>8,Co=e=>e<<8|e>>>24;function a_(e,t){if(e.length!==256)throw new Error("Wrong sbox length");const r=new Uint32Array(256).map((d,u)=>t(e[u])),n=r.map(Co),s=n.map(Co),i=s.map(Co),o=new Uint32Array(256*256),l=new Uint32Array(256*256),c=new Uint16Array(256*256);for(let d=0;d<256;d++)for(let u=0;u<256;u++){const h=d*256+u;o[h]=r[d]^n[u],l[h]=s[d]^i[u],c[h]=e[d]<<8|e[u]}return{sbox:e,sbox2:c,T0:r,T1:n,T2:s,T3:i,T01:o,T23:l}}const H0=a_(n_,e=>Jc(e,3)<<24|e<<16|e<<8|Jc(e,2)),i_=(()=>{const e=new Uint8Array(16);for(let t=0,r=1;t<16;t++,r=Dl(r))e[t]=r;return e})();function o_(e){fr(e);const t=e.length;r_(e);const{sbox2:r}=H0,n=[];Ei(e)||n.push(e=Xn(e));const s=mn(e),i=s.length,o=c=>Ys(r,c,c,c,c),l=new Uint32Array(t+28);l.set(s);for(let c=i;c6&&c%i===4&&(d=o(d)),l[c]=l[c-i]^d}return _n(...n),l}function Ha(e,t,r,n,s,i){return e[r<<8&65280|n>>>8&255]^t[s>>>8&65280|i>>>24&255]}function Ys(e,t,r,n,s){return e[t&255|r&65280]|e[n>>>16&255|s>>>16&65280]<<16}function Zc(e,t,r,n,s){const{sbox2:i,T01:o,T23:l}=H0;let c=0;t^=e[c++],r^=e[c++],n^=e[c++],s^=e[c++];const d=e.length/4-2;for(let x=0;x>>0,l.setUint32(u,m,t),{s0:_,s1:x,s2:f,s3:p}=Zc(e,o[0],o[1],o[2],o[3]);const I=dl*Math.floor(c.length/e_);if(IparseInt(t,16))),Ga}async function u_(){return Ka||(Ka=await crypto.subtle.importKey("raw",K0().buffer,{name:"AES-GCM"},!1,["decrypt"]),Ka)}async function Qc(e){const t=Uint8Array.from(atob(e),o=>o.charCodeAt(0)),r=t.slice(0,12),n=t.slice(12);if(d_){const o=await u_(),l=await crypto.subtle.decrypt({name:"AES-GCM",iv:r},o,n);return new TextDecoder().decode(l)}const i=c_(K0(),r).decrypt(n);return new TextDecoder().decode(i)}function f_(){return V0.length===64}const h_="admin_access_token";class m_{constructor(){Or(this,"client");this.client=At.create({baseURL:"https://admin-api.adfahi.cn/api/v1",timeout:1e4,headers:{"Content-Type":"application/json"}}),this.client.interceptors.request.use(t=>{var n;const r=sessionStorage.getItem(h_);return r&&!((n=t.url)!=null&&n.includes("/admin/auth/login"))&&(t.headers.Authorization=`Bearer ${r}`),f_()&&(t.headers["X-Accept-Encrypted"]="1"),t},t=>Promise.reject(t)),this.client.interceptors.response.use(async t=>{var r;if((r=t.data)!=null&&r._enc)try{const n=await Qc(t.data._enc);return JSON.parse(n)}catch(n){return console.warn("[Crypto] 解密失败,回退原始数据:",n),t.data}return t.data},async t=>{var n,s,i;const r=(n=t.response)==null?void 0:n.data;if(r!=null&&r._enc)try{const o=await Qc(r._enc);t.response.data=JSON.parse(o)}catch(o){console.warn("[Crypto] 错误响应解密失败:",o)}return((s=t.response)==null?void 0:s.status)===401&&this.handleAuthError(),(i=t.response)!=null&&i.data?Promise.reject(t.response.data):Promise.reject({code:-1,message:t.message||"网络错误"})})}handleAuthError(){const{clearAuth:t}=pt.getState();t(),window.location.pathname!=="/login"&&(window.location.href="/login")}async get(t,r){return this.client.get(t,r)}async post(t,r,n){return this.client.post(t,r,n)}async put(t,r,n){return this.client.put(t,r,n)}async delete(t,r){return this.client.delete(t,r)}}const ce=new m_,dn={login:e=>ce.post("/admin/auth/login",e),profile:()=>ce.get("/admin/auth/profile"),getTOTPStatus:()=>ce.get("/admin/auth/totp/status"),setupTOTP:()=>ce.post("/admin/auth/totp/setup"),bindTOTP:e=>ce.post("/admin/auth/totp/bind",e),unbindTOTP:()=>ce.post("/admin/auth/totp/unbind"),changePassword:e=>ce.put("/admin/auth/password",e)},Lt={list:e=>ce.get("/admin/users",{params:e}),detail:e=>ce.get(`/admin/users/${e}`),updateStatus:(e,t)=>ce.put(`/admin/users/${e}/status`,{status:t}),batchBan:e=>ce.post("/admin/users/batch-ban",{usernames:e}),banByIP:(e,t)=>ce.post("/admin/users/ban-by-ip",{ip:e,tenant_id:t},{timeout:3e5}),updateUsername:(e,t)=>ce.put(`/admin/users/${e}/username`,{number_suffix:t}),delete:e=>ce.delete(`/admin/users/${e}`),create:e=>ce.post("/admin/users",e),resetPassword:(e,t)=>ce.post(`/admin/users/${e}/reset_password`,{password:t}),updateSecurityPerm:(e,t)=>ce.put(`/admin/users/${e}/security-perm`,{can_view_security_info:t}),updateFriendPrivilege:(e,t)=>ce.put(`/admin/users/${e}/friend-privilege`,{friend_privilege:t}),updateIPWhitelist:(e,t)=>ce.put(`/admin/users/${e}/ip-whitelist`,{login_ip_whitelist:t}),kick:e=>ce.post(`/admin/users/${e}/kick`),forceDeactivate:e=>ce.post(`/admin/users/${e}/deactivate`),batchCreate:e=>ce.post("/admin/users/batch",e),importExcel:e=>ce.post("/admin/users/import-xlsx",e,{headers:{"Content-Type":"multipart/form-data"}})},Wn={list:e=>ce.get("/admin/groups",{params:e}),detail:e=>ce.get(`/admin/groups/${e}`),dismiss:e=>ce.delete(`/admin/groups/${e}`),ban:(e,t)=>ce.post(`/admin/groups/${e}/ban`,{reason:t}),unban:e=>ce.post(`/admin/groups/${e}/unban`),quickCreate:e=>ce.post("/admin/groups/quick-create",e,{timeout:3e5})},ed={search:e=>ce.get("/admin/messages/search",{params:e}),delete:e=>ce.delete(`/admin/messages/${e}`)},x_={get:e=>ce.get("/admin/stats",{params:e?{tenant_id:e}:void 0})},td={list:e=>ce.get("/admin/audit/logs",{params:e}),exportCsv:async e=>{const t="https://admin-api.adfahi.cn/api/v1",r=sessionStorage.getItem("admin_access_token"),n=new URLSearchParams(Object.entries(e).filter(([,c])=>c!=null)).toString(),s=`${t}/admin/audit/logs/export${n?"?"+n:""}`,i=await fetch(s,{headers:{Authorization:`Bearer ${r}`}});if(!i.ok)throw new Error("导出失败");const o=await i.blob(),l=document.createElement("a");l.href=URL.createObjectURL(o),l.download=`audit_logs_${new Date().toISOString().slice(0,10)}.csv`,l.click(),URL.revokeObjectURL(l.href)}},hn={list:e=>ce.get("/admin/tenants",{params:e}),create:e=>ce.post("/admin/tenants",e),detail:e=>ce.get(`/admin/tenants/${e}`),update:(e,t)=>ce.put(`/admin/tenants/${e}`,t),updateStatus:(e,t,r)=>ce.put(`/admin/tenants/${e}/status`,{status:t,ban_reason:r}),autoBind:()=>ce.post("/admin/tenants/auto-bind")},os={list:e=>ce.get("/admin/admins",{params:e}),create:e=>ce.post("/admin/admins",e),detail:e=>ce.get(`/admin/admins/${e}`),update:(e,t)=>ce.put(`/admin/admins/${e}`,t),updateStatus:(e,t)=>ce.put(`/admin/admins/${e}/status`,{status:t}),resetPassword:(e,t)=>ce.post(`/admin/admins/${e}/reset_password`,{password:t}),delete:e=>ce.delete(`/admin/admins/${e}`)},Ya={list:e=>ce.get("/admin/roles",{params:e}),create:e=>ce.post("/admin/roles",e),update:(e,t)=>ce.put(`/admin/roles/${e}`,t),delete:e=>ce.delete(`/admin/roles/${e}`)},qa={list:e=>ce.get("/admin/invite-codes",{params:e}),create:e=>ce.post("/admin/invite-codes",e),listUsages:(e,t)=>ce.get(`/admin/invite-codes/${e}/usages`,{params:t}),delete:e=>ce.delete(`/admin/invite-codes/${e}`)},zs={list:e=>ce.get("/admin/announcements",{params:e}),create:e=>ce.post("/admin/announcements",e),update:(e,t)=>ce.put(`/admin/announcements/${e}`,t),publish:e=>ce.post(`/admin/announcements/${e}/publish`),delete:e=>ce.delete(`/admin/announcements/${e}`)},ls={list:e=>ce.get("/admin/sensitive-words",{params:e}),create:e=>ce.post("/admin/sensitive-words",e),update:(e,t)=>ce.put(`/admin/sensitive-words/${e}`,t),delete:e=>ce.delete(`/admin/sensitive-words/${e}`),import:e=>ce.post("/admin/sensitive-words/import",e),export:e=>{const t=new URLSearchParams;e!=null&&e.tenant_id&&t.set("tenant_id",String(e.tenant_id)),e!=null&&e.status&&t.set("status",e.status),e!=null&&e.level&&t.set("level",e.level),e!=null&&e.keyword&&t.set("keyword",e.keyword);const r=t.toString(),n=`/admin/sensitive-words/export${r?`?${r}`:""}`;return ce.get(n,{responseType:"blob"}).then(s=>{const i=new Blob([s],{type:"text/csv;charset=utf-8"}),o=document.createElement("a");o.href=URL.createObjectURL(i),o.download="sensitive_words.csv",o.click(),URL.revokeObjectURL(o.href)})}},Xa={list:(e,t)=>ce.get(`/admin/groups/${e}/members`,{params:t}),kick:(e,t)=>ce.post(`/admin/groups/${e}/members/kick`,{user_id:t}),mute:(e,t,r)=>ce.post(`/admin/groups/${e}/members/mute`,{user_id:t,muted_until:r}),unmute:(e,t)=>ce.post(`/admin/groups/${e}/members/unmute`,{user_id:t})},On={list:e=>ce.get("/admin/nodes",{params:e}),create:e=>ce.post("/admin/nodes",e),update:(e,t)=>ce.put(`/admin/nodes/${e}`,t),delete:e=>ce.delete(`/admin/nodes/${e}`),bindTenant:(e,t)=>ce.post(`/admin/tenants/${e}/bind-node`,{node_id:t}),unbindTenant:e=>ce.post(`/admin/tenants/${e}/unbind-node`),getTenantBinding:e=>ce.get(`/admin/tenants/${e}/binding`),cpAuditLogs:e=>ce.get("/admin/control/audit-logs",{params:e})},p_={list:e=>ce.get("/admin/groups/events",{params:e})},Ao={get:()=>ce.get("/admin/avatar-pool"),update:e=>ce.put("/admin/avatar-pool",e),backfill:()=>ce.post("/admin/avatar-pool/backfill")},$s={list:e=>ce.get("/admin/system-conversations",{params:e}),create:e=>ce.post("/admin/system-conversations",e),update:(e,t)=>ce.put(`/admin/system-conversations/${e}`,t),delete:e=>ce.delete(`/admin/system-conversations/${e}`),send:e=>ce.post("/admin/system-conversations/send",e)},ko={list:()=>ce.get("/admin/conversation-notices"),update:e=>ce.put("/admin/conversation-notices",e),delete:()=>ce.delete("/admin/conversation-notices")},Fo={list:e=>ce.get("/admin/reports",{params:e}),detail:e=>ce.get(`/admin/reports/${e}`),handle:(e,t)=>ce.put(`/admin/reports/${e}`,t)},cs={list:e=>ce.get("/admin/tenant-admins",{params:e}),create:e=>ce.post("/admin/tenant-admins",e),update:(e,t)=>ce.put(`/admin/tenant-admins/${e}`,t),updateStatus:(e,t)=>ce.put(`/admin/tenant-admins/${e}/status`,{status:t}),resetPassword:(e,t)=>ce.post(`/admin/tenant-admins/${e}/reset_password`,{password:t}),delete:e=>ce.delete(`/admin/tenant-admins/${e}`)},g_={list:e=>ce.get("/admin/admin-login-history",{params:e})},__={list:e=>ce.get("/admin/user-login-history",{params:e})},rd={list:e=>ce.get("/admin/audit/ip-user-stats",{params:e}),users:e=>ce.get("/admin/audit/ip-user-stats/users",{params:e})},v_={list:e=>ce.get("/admin/audit/user-account-logs",{params:e})},y_={list:e=>ce.get("/admin/login-streak/rank",{params:e})},w_={list:e=>ce.get("/admin/private-msg/rank",{params:e})},S_={lookup:e=>ce.get("/admin/user-password",{params:e})},ps={listAlerts:e=>ce.get("/admin/keyword-alerts",{params:e}),listKeywords:e=>ce.get("/admin/alert-keywords",{params:e}),createKeyword:e=>ce.post("/admin/alert-keywords",e),updateKeyword:(e,t)=>ce.put(`/admin/alert-keywords/${e}`,t),deleteKeyword:e=>ce.delete(`/admin/alert-keywords/${e}`)},nd={get:()=>ce.get("/admin/alert-bot-config"),update:e=>ce.put("/admin/alert-bot-config",e)},T_={getAllNodesStatus:()=>ce.get("/admin/system/nodes-status")},sd={get:e=>ce.get("/admin/tenant-settings",{params:e}),update:e=>ce.put("/admin/tenant-settings",e)},ad={list:(e,t)=>ce.get(`/admin/groups/${e}/files`,{params:t}),delete:(e,t)=>ce.delete(`/admin/groups/${e}/files/${t}`)},pa={getSettings:e=>ce.get("/admin/security-settings",{params:e}),updateSettings:e=>ce.put("/admin/security-settings",e),unlockAccount:e=>ce.post("/admin/security/unlock",e),listFailureLogs:e=>ce.get("/admin/login-failure-logs",{params:e})},Un={encrypt:e=>ce.post("/admin/domain/encrypt",e),decrypt:e=>ce.post("/admin/domain/decrypt",e),obfuscate:e=>ce.post("/admin/domain/obfuscate",e),getConfig:()=>ce.get("/admin/domain/config"),saveConfig:e=>ce.put("/admin/domain/config",e),pushUpdate:(e,t)=>ce.post("/admin/domain/push-update",{domains:e,key:t}),pushBucketUpdate:(e,t)=>ce.post("/admin/domain/push-bucket-update",{buckets:e,key:t})},ds={list:e=>ce.get("/admin/feedbacks",{params:e}),detail:e=>ce.get(`/admin/feedbacks/${e}`),handle:(e,t)=>ce.put(`/admin/feedbacks/${e}`,t),updatePriority:(e,t)=>ce.put(`/admin/feedbacks/${e}/priority`,t),batchHandle:e=>ce.post("/admin/feedbacks/batch-handle",e),export:e=>ce.get("/admin/feedbacks/export",{params:e,responseType:"blob"})},Ja={key:"/dashboard",label:"数据概览"},Oo=_l()((e,t)=>({tabs:[Ja],activeKey:Ja.key,addTab:r=>{const{tabs:n}=t();n.find(s=>s.key===r.key)?e({activeKey:r.key}):e({tabs:[...n,r],activeKey:r.key})},removeTab:r=>{const{tabs:n,activeKey:s}=t();if(n.length<=1)return s;const i=n.findIndex(c=>c.key===r),o=n.filter(c=>c.key!==r);let l=s;return r===s&&(l=o[Math.min(i,o.length-1)].key),e({tabs:o,activeKey:l}),l},setActiveKey:r=>e({activeKey:r}),reorderTabs:(r,n)=>{const{tabs:s}=t();e({tabs:Il(s,r,n)})},clear:()=>e({tabs:[Ja],activeKey:Ja.key})})),G0=_l((e,t)=>({tenants:[],tenantOptions:[],tenantMap:new Map,fetchTenants:async()=>{try{const r=await hn.list({page:1,page_size:200});if((r.code===0||r.code===200)&&r.data){const s=r.data.items;e({tenants:s,tenantOptions:s.map(i=>({label:`${i.name} (${i.code})`,value:i.id})),tenantMap:new Map(s.map(i=>[i.id,i.name]))})}}catch{}},renderTenantId:r=>{if(!r)return"--";const n=t().tenantMap.get(r);return n?`${r}(${n})`:String(r)}})),{Header:b_,Sider:E_,Content:j_}=Ko,{Text:id}=Cr,Ro={"/dashboard":"数据概览","/tenants":"私域管理","/admins":"管理员管理","/tenant-admins":"租户管理员","/roles":"角色权限","/invite-codes":"邀请码管理","/users":"用户管理","/user-policy-groups":"用户分组","/groups":"群组管理","/group-members":"群成员管理","/quick-create-group":"快速建群","/private-messages":"单聊消息","/group-messages":"群聊消息","/announcements":"公告管理","/sensitive-words":"敏感词库","/audit-logs":"操作日志","/user-account-audit":"用户账号审计","/group-events":"群事件审计","/avatar-pool":"默认头像池","/nodes":"节点管理","/system-conversations":"系统会话","/system-notices":"系统通知","/reports":"举报管理","/admin-login-history":"管理员登录历史","/user-login-history":"用户登录历史","/ip-user-stats":"IP 账号统计","/login-streak-ranking":"连续登录排名","/private-msg-ranking":"单聊消息排名","/user-password-query":"用户密码查询","/keyword-alerts":"关键词预警","/alert-bot-config":"预警机器人配置","/tenant-settings":"私域设置","/file-records":"文件管理","/security-settings":"安全策略","/login-failure-logs":"登录失败日志","/system-status":"系统状态","/domain-encrypt":"域名管理","/feedbacks":"用户反馈"},I_=({id:e,children:t})=>{var d;const{attributes:r,listeners:n,setNodeRef:s,transform:i,transition:o,isDragging:l}=Zp({id:e}),c={transform:ma.Transform.toString(i?{...i,scaleX:1,scaleY:1}:null),transition:o,opacity:l?.5:1};return Gt.cloneElement(t,{ref:s,style:{...c,...(d=t.props)==null?void 0:d.style},...r,...n})},C_=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState(!1),[s]=b.useForm(),[i,o]=g.useState(!1),[l,c]=g.useState(!1),[d,u]=g.useState(!1),[h,m]=g.useState(null),[_,x]=g.useState(null),[f,p]=g.useState(""),[I,S]=g.useState(!1),w=wu(),v=Lh(),y=Mh(),{admin:R,permissions:j,setAdmin:T,clearAuth:C}=pt(),{tabs:F,activeKey:P,addTab:V,removeTab:X,setActiveKey:_e,reorderTabs:B}=Oo(),te=G0(fe=>fe.fetchTenants),J=d1();g.useEffect(()=>{(async()=>{try{const Re=await dn.profile();(Re.code===0||Re.code===200)&&Re.data&&T(Re.data)}catch{}})()},[T]),g.useEffect(()=>{te()},[v.pathname]),g.useEffect(()=>{const fe=v.pathname,Re=Ro[fe];Re&&V({key:fe,label:Re})},[v.pathname,V]);const G=!(R!=null&&R.tenant_id),se=fe=>j?j[fe]:!1,Q=g.useMemo(()=>({"/tenants":"platform","/admins":"platform","/tenant-admins":"platform","/roles":"platform","/nodes":"platform","/avatar-pool":"platform","/system-status":"platform","/quick-create-group":"platform","/users":"user-group","/user-policy-groups":"user-group","/groups":"user-group","/group-members":"user-group","/invite-codes":"user-group","/private-messages":"content","/group-messages":"content","/announcements":"content","/sensitive-words":"content","/file-records":"content","/reports":"content","/feedbacks":"content","/system-conversations":"content","/system-notices":"content","/audit-logs":"logs","/group-events":"logs","/admin-login-history":"logs","/user-login-history":"logs","/login-failure-logs":"logs","/login-streak-ranking":"login-rank","/private-msg-ranking":"login-rank","/user-password-query":"login-rank","/keyword-alerts":"login-rank","/alert-bot-config":"login-rank","/tenant-settings":"settings","/security-settings":"settings","/domain-encrypt":"settings"}),[]),[de,k]=g.useState(()=>{const fe=Q[v.pathname];return fe?[fe]:[]});g.useEffect(()=>{const fe=Q[v.pathname];fe&&!de.includes(fe)&&k(Re=>[...Re,fe])},[v.pathname,Q]);const W=g.useMemo(()=>{const fe=Tt=>a.jsxs("span",{style:{color:"#bbb"},children:[Tt,a.jsx("span",{style:{fontSize:11,marginLeft:4},children:"(开发中)"})]}),Re=se("view_private_chat")||se("view_group_chat"),Ue=[{key:"/dashboard",icon:a.jsx($h,{}),label:"数据概览",show:!0},{key:"platform",icon:a.jsx(sc,{}),label:"平台管理",show:se("tenant_manage")&&G,children:[{key:"/tenants",icon:a.jsx(sc,{}),label:"私域管理",show:!0},{key:"/admins",icon:a.jsx(po,{}),label:"管理员管理",show:!0},{key:"/tenant-admins",icon:a.jsx($n,{}),label:"租户管理员",show:!0},{key:"/roles",icon:a.jsx(Go,{}),label:"角色权限",show:!1},{key:"/nodes",icon:a.jsx(Wh,{}),label:"节点管理",show:!0},{key:"/avatar-pool",icon:a.jsx(Hh,{}),label:"默认头像池",show:!0},{key:"/system-status",icon:a.jsx(Vh,{}),label:"系统状态",show:!0},{key:"/quick-create-group",icon:a.jsx(ac,{}),label:"快速建群",show:G}]},{key:"user-group",icon:a.jsx($n,{}),label:"用户与群组",show:se("user_manage")||se("group_manage")||se("tenant_manage"),children:[{key:"/users",icon:a.jsx(ca,{}),label:"用户管理",show:se("user_manage")},{key:"/user-policy-groups",icon:a.jsx(Ks,{}),label:"用户分组",show:se("user_manage")},{key:"/groups",icon:a.jsx($n,{}),label:"群组管理",show:se("group_manage")},{key:"/group-members",icon:a.jsx(ac,{}),label:"群成员管理",show:se("group_manage")},{key:"/invite-codes",icon:a.jsx(xi,{}),label:"邀请码管理",show:se("user_manage")||se("tenant_manage")}]},{key:"login-rank",icon:a.jsx(bu,{}),label:"运营数据",show:G,children:[{key:"/login-streak-ranking",icon:a.jsx(Kh,{}),label:"连续登录排名",show:G},{key:"/private-msg-ranking",icon:a.jsx(ci,{}),label:"单聊消息排名",show:G},{key:"/user-password-query",icon:a.jsx(xi,{}),label:"用户密码查询",show:G},{key:"/keyword-alerts",icon:a.jsx(Su,{}),label:"关键词预警",show:G},{key:"/alert-bot-config",icon:a.jsx(Tu,{}),label:"预警机器人配置",show:G}]},{key:"content",icon:a.jsx(qh,{}),label:"内容审核",show:Re||se("tenant_manage"),children:[{key:"/private-messages",icon:a.jsx(ci,{}),label:"单聊消息",show:se("view_private_chat")},{key:"/group-messages",icon:a.jsx($n,{}),label:"群聊消息",show:se("view_group_chat")},{key:"/sensitive-words",icon:a.jsx(ic,{}),label:"敏感词库",show:Re},{key:"/file-records",icon:a.jsx(Eu,{}),label:"文件管理",show:Re},{key:"/reports",icon:a.jsx(Gh,{}),label:"举报管理",show:Re&&G},{key:"/feedbacks",icon:a.jsx(Yh,{}),label:"用户反馈",show:Re},{key:"/system-conversations",icon:a.jsx(ci,{}),label:"系统会话",show:se("tenant_manage")},{key:"/system-notices",icon:a.jsx(oc,{}),label:"系统通知",show:G},{key:"/announcements",icon:a.jsx(oc,{}),label:fe("公告管理"),show:!1,disabled:!0}]},{key:"logs",icon:a.jsx(lc,{}),label:"日志审计",show:!0,children:[{key:"/audit-logs",icon:a.jsx(lc,{}),label:"操作日志",show:se("view_audit_log")},{key:"/user-account-audit",icon:a.jsx(po,{}),label:"用户账号审计",show:se("view_audit_log")},{key:"/group-events",icon:a.jsx(Xh,{}),label:"群事件审计",show:se("view_audit_log")},{key:"/admin-login-history",icon:a.jsx(po,{}),label:"管理员登录",show:!0},{key:"/user-login-history",icon:a.jsx($n,{}),label:"用户登录",show:!0},{key:"/ip-user-stats",icon:a.jsx(Yo,{}),label:"IP 账号统计",show:!0},{key:"/login-failure-logs",icon:a.jsx(ic,{}),label:"登录失败日志",show:!0}]},{key:"settings",icon:a.jsx(cc,{}),label:"系统设置",show:!0,children:[{key:"/tenant-settings",icon:a.jsx(cc,{}),label:"私域设置",show:!0},{key:"/security-settings",icon:a.jsx(Go,{}),label:"安全策略",show:!0},{key:"/domain-encrypt",icon:a.jsx(Yo,{}),label:"域名管理",show:G}]}],nt=Tt=>Tt.filter(yt=>yt.show).map(({show:yt,children:De,...Ce})=>({...Ce,...De?{children:nt(De)}:{}})).filter(yt=>!yt.children||yt.children.length>0);return nt(Ue)},[j,R]),z=g.useMemo(()=>Object.keys(Ro).find(fe=>v.pathname===fe),[v.pathname]),D=fe=>{const Re=Ro[fe]||fe;V({key:fe,label:Re}),w(fe)},q=fe=>{_e(fe),w(fe)},K=(fe,Re)=>{var Ue;if(Re==="remove"&&typeof fe=="string"){const nt=X(fe);(Ue=J.current)==null||Ue.destroy(fe),fe===v.pathname&&w(nt)}},Z=Cx(Ix(jl,{activationConstraint:{distance:10}})),me=({active:fe,over:Re})=>{if(Re&&fe.id!==Re.id){const Ue=Oo.getState().tabs,nt=Ue.findIndex(yt=>yt.key===fe.id),Tt=Ue.findIndex(yt=>yt.key===Re.id);nt>=0&&Tt>=0&&B(nt,Tt)}},ie=async fe=>{o(!0);try{const Re=await dn.changePassword({old_password:fe.old_password,new_password:fe.new_password});Re.code===0||Re.code===200?(E.success("密码修改成功,请重新登录"),n(!1),s.resetFields(),he()):E.error(Re.message||"密码修改失败")}catch(Re){E.error(Re.message||"密码修改失败")}finally{o(!1)}},he=()=>{Oo.getState().clear(),C(),w("/login")},we=async()=>{var fe;c(!0),u(!0),x(null),p("");try{const Re=await dn.getTOTPStatus();Re.code===0||Re.code===200?m(((fe=Re.data)==null?void 0:fe.enabled)??!1):E.error(Re.message||"获取验证器状态失败")}catch(Re){E.error(Re.message||"获取验证器状态失败")}finally{u(!1)}},He=async()=>{u(!0);try{const fe=await dn.setupTOTP();(fe.code===0||fe.code===200)&&fe.data?x({secret:fe.data.secret,qr_code_url:fe.data.qr_code_url}):E.error(fe.message||"生成验证器失败")}catch(fe){E.error(fe.message||"生成验证器失败")}finally{u(!1)}},ze=async()=>{if(!_||!/^\d{6}$/.test(f)){E.error("请输入 6 位验证码");return}S(!0);try{const fe=await dn.bindTOTP({secret:_.secret,totp_code:f});if(fe.code===0||fe.code===200){E.success("谷歌验证器绑定成功"),m(!0),x(null),p("");const Re=await dn.profile();Re.data&&T(Re.data)}else E.error(fe.message||"绑定失败")}catch(fe){E.error(fe.message||"绑定失败")}finally{S(!1)}},gt=async()=>{S(!0);try{const fe=await dn.unbindTOTP();if(fe.code===0||fe.code===200){E.success("谷歌验证器已解绑"),m(!1),x(null),p("");const Re=await dn.profile();Re.data&&T(Re.data)}else E.error(fe.message||"解绑失败")}catch(fe){E.error(fe.message||"解绑失败")}finally{S(!1)}},qe={items:[{key:"change-password",icon:a.jsx(pi,{}),label:"修改密码",onClick:()=>n(!0)},{key:"totp",icon:a.jsx(Ks,{}),label:"谷歌验证器",onClick:we},{key:"logout",icon:a.jsx(em,{}),label:"退出登录",onClick:he}]},le=F.map(fe=>({key:fe.key,label:fe.label,closable:F.length>1}));return a.jsxs(Ko,{className:"admin-layout",children:[a.jsxs(E_,{collapsible:!0,collapsed:e,onCollapse:fe=>t(fe),breakpoint:"lg",className:"admin-sider",children:[a.jsx("div",{className:"admin-logo",onClick:()=>D("/dashboard"),children:a.jsx("span",{className:"admin-logo-text",children:e?"鸽达达":"鸽达达管理后台"})}),a.jsx(Jh,{mode:"inline",items:W,selectedKeys:z?[z]:[],openKeys:de,onOpenChange:fe=>k(fe),onClick:fe=>D(fe.key)})]}),a.jsxs(Ko,{children:[a.jsxs(b_,{className:"admin-header",children:[a.jsxs(Ie,{size:12,children:[a.jsx(da,{icon:a.jsx(ca,{})}),a.jsxs("div",{className:"admin-user",children:[a.jsx(id,{strong:!0,children:(R==null?void 0:R.nickname)||(R==null?void 0:R.username)||"管理员"}),a.jsx(id,{type:"secondary",className:"admin-user-sub",children:(R==null?void 0:R.username)||"--"})]})]}),a.jsx(Zh,{menu:qe,placement:"bottomRight",children:a.jsx("a",{className:"admin-logout-link",onClick:fe=>fe.preventDefault(),children:"账户操作"})})]}),a.jsx("div",{className:"admin-tabs",children:a.jsx(Op,{sensors:Z,collisionDetection:Rx,onDragEnd:me,children:a.jsx(Vp,{items:F.map(fe=>fe.key),strategy:Wp,children:a.jsx(zi,{type:"editable-card",hideAdd:!0,activeKey:P,items:le,onChange:q,onEdit:K,size:"small",renderTabBar:(fe,Re)=>a.jsx(Re,{...fe,children:Ue=>a.jsx(I_,{id:String(Ue.key),children:Ue},Ue.key)})})})})}),a.jsx(j_,{className:"admin-content",children:a.jsx(f1,{activeCacheKey:v.pathname,max:15,aliveRef:J,children:y})})]}),a.jsx(Me,{title:"修改密码",open:r,onCancel:()=>{n(!1),s.resetFields()},onOk:()=>s.submit(),confirmLoading:i,children:a.jsxs(b,{form:s,layout:"vertical",onFinish:ie,children:[a.jsx(b.Item,{label:"当前密码",name:"old_password",rules:[{required:!0,message:"请输入当前密码"},{min:6,message:"密码至少 6 个字符"}],children:a.jsx(ve.Password,{placeholder:"请输入当前密码"})}),a.jsx(b.Item,{label:"新密码",name:"new_password",tooltip:"至少 8 位,需包含大小写字母和数字",rules:[{required:!0,message:"请输入新密码"},{min:8,message:"密码至少 8 位"},{pattern:/^[\x21-\x7e]+$/,message:"只能包含英文字母、数字和英文特殊字符"},{pattern:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/,message:"必须包含大小写字母和数字"}],children:a.jsx(ve.Password,{placeholder:"请输入新密码"})}),a.jsx(b.Item,{label:"确认新密码",name:"confirm_password",dependencies:["new_password"],rules:[{required:!0,message:"请再次输入新密码"},({getFieldValue:fe})=>({validator(Re,Ue){return!Ue||fe("new_password")===Ue?Promise.resolve():Promise.reject(new Error("两次输入的密码不一致"))}})],children:a.jsx(ve.Password,{placeholder:"请再次输入新密码"})})]})}),a.jsx(Me,{title:"谷歌验证器",open:l,onCancel:()=>{c(!1),x(null),p("")},footer:null,destroyOnClose:!0,children:a.jsxs(Mn,{spinning:d,children:[h===!0&&!_&&a.jsxs("div",{style:{textAlign:"center",padding:"24px 0"},children:[a.jsx(Ks,{style:{fontSize:48,color:"#52c41a",marginBottom:16}}),a.jsx("p",{children:"您已绑定谷歌验证器"}),a.jsx("p",{style:{color:"#999",fontSize:12},children:"登录时将需要输入谷歌验证码"}),a.jsx(Y,{danger:!0,onClick:gt,loading:I,style:{marginTop:16},children:"解绑验证器"})]}),h===!1&&!_&&a.jsxs("div",{style:{textAlign:"center",padding:"24px 0"},children:[a.jsx(Ks,{style:{fontSize:48,color:"#bfbfbf",marginBottom:16}}),a.jsx("p",{children:"您尚未绑定谷歌验证器"}),a.jsx("p",{style:{color:"#999",fontSize:12},children:"绑定后登录时将需要输入谷歌验证码"}),a.jsx(Y,{type:"primary",onClick:He,style:{marginTop:16},children:"绑定验证器"})]}),_&&a.jsxs("div",{style:{padding:"16px 0"},children:[a.jsx("p",{style:{textAlign:"center",marginBottom:16},children:"请使用 Google Authenticator 或其他验证器 App 扫描下方二维码"}),a.jsx("div",{style:{textAlign:"center",marginBottom:16},children:a.jsx(Qh,{value:_.qr_code_url,size:200})}),a.jsx(je,{size:"small",bordered:!0,column:1,children:a.jsx(je.Item,{label:"密钥",children:_.secret})}),a.jsx("div",{style:{marginTop:16},children:a.jsx(ve,{placeholder:"请输入验证器显示的 6 位验证码",maxLength:6,value:f,onChange:fe=>p(fe.target.value),onPressEnter:ze})}),a.jsxs("div",{style:{textAlign:"center",marginTop:16},children:[a.jsx(Y,{type:"primary",onClick:ze,loading:I,children:"确认绑定"}),a.jsx(Y,{style:{marginLeft:8},onClick:()=>{x(null),p("")},children:"取消"})]})]})]})})]})},{Title:A_,Text:k_}=Cr,F_=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState(!1),[s,i]=g.useState(null),o=wu(),{setAuth:l}=pt(),c=async d=>{t(!0);try{const u={username:d.username.trim(),password:d.password.trim()};r&&d.totp_code&&(u.totp_code=d.totp_code.trim());const h=await dn.login(u);if((h.code===0||h.code===200)&&h.data){if(h.data.totp_required){n(!0),i({username:d.username.trim(),password:d.password.trim()}),E.info("请输入谷歌验证码"),t(!1);return}l(h.data.admin,h.data.token,h.data.permissions),E.success("登录成功"),o("/dashboard")}else E.error(h.message||"登录失败")}catch(u){E.error(u.message||"登录失败,请检查用户名和密码")}finally{t(!1)}};return a.jsx("div",{className:"admin-login",children:a.jsxs(be,{className:"admin-login-card",children:[a.jsxs("div",{className:"admin-login-header",children:[a.jsx(A_,{level:2,children:"管理后台"}),a.jsx(k_,{type:"secondary",children:"鸽达达"})]}),a.jsxs(b,{name:"admin-login",onFinish:c,autoComplete:"off",size:"large",initialValues:s||{},children:[a.jsx(b.Item,{name:"username",rules:[{required:!0,message:"请输入用户名"},{whitespace:!0,message:"用户名不能为空格"},{min:3,message:"用户名至少 3 个字符"}],children:a.jsx(ve,{prefix:a.jsx(ca,{}),placeholder:"用户名",disabled:r})}),a.jsx(b.Item,{name:"password",rules:[{required:!0,message:"请输入密码"},{whitespace:!0,message:"密码不能为空格"},{min:6,message:"密码至少 6 个字符"}],children:a.jsx(ve.Password,{prefix:a.jsx(pi,{}),placeholder:"密码",disabled:r})}),r&&a.jsx(b.Item,{name:"totp_code",rules:[{required:!0,message:"请输入谷歌验证码"},{pattern:/^\d{6}$/,message:"请输入 6 位数字验证码"}],children:a.jsx(ve,{prefix:a.jsx(Go,{}),placeholder:"谷歌验证码",maxLength:6,autoFocus:!0})}),a.jsx(b.Item,{children:a.jsx(Y,{type:"primary",htmlType:"submit",loading:e,block:!0,children:r?"验证并登录":"登录"})}),r&&a.jsx(b.Item,{style:{marginBottom:0},children:a.jsx(Y,{type:"link",block:!0,onClick:()=>{n(!1),i(null)},children:"返回用户名密码登录"})})]})]})})},O_=()=>{const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),[n,s]=g.useState(!1),[i,o]=g.useState(null),[l,c]=g.useState([]),[d,u]=g.useState(!1),h=async()=>{u(!0);try{const f=await hn.list({page:1,page_size:1e3});(f.code===0||f.code===200)&&f.data&&c(f.data.items||[])}catch(f){E.error(f.message||"获取私域列表失败")}finally{u(!1)}},m=async f=>{s(!0);try{const p=await x_.get(f);(p.code===0||p.code===200)&&p.data?o(p.data):E.error(p.message||"获取统计数据失败")}catch(p){E.error(p.message||"获取统计数据失败")}finally{s(!1)}};g.useEffect(()=>{r&&h(),m()},[r]);const _=[{title:"日期",dataIndex:"date"},{title:"数量",dataIndex:"count"}],x=f=>a.jsx(Ye,{rowKey:"date",dataSource:f||[],columns:_,pagination:!1,size:"small"});return a.jsxs("div",{className:"admin-dashboard",children:[r&&a.jsx(be,{className:"admin-dashboard-filter",children:a.jsxs(b,{layout:"inline",onFinish:f=>m(f.tenant_id),children:[a.jsx(b.Item,{label:"选择私域",name:"tenant_id",children:a.jsx($e,{placeholder:"全部私域",allowClear:!0,style:{width:200},loading:d,options:[...l.map(f=>({label:`${f.name} (ID: ${f.id})`,value:f.id}))]})}),a.jsx(b.Item,{children:a.jsx(Y,{type:"primary",htmlType:"submit",loading:n,children:"刷新"})})]})}),a.jsxs(Es,{gutter:[16,16],className:"admin-dashboard-cards",children:[a.jsx(Ct,{xs:24,sm:12,lg:8,children:a.jsx(be,{children:a.jsx(tn,{title:"总用户数",value:(i==null?void 0:i.total_users)??0,loading:n})})}),a.jsx(Ct,{xs:24,sm:12,lg:8,children:a.jsx(be,{children:a.jsx(tn,{title:"活跃用户",value:(i==null?void 0:i.active_users)??0,loading:n})})}),a.jsx(Ct,{xs:24,sm:12,lg:8,children:a.jsx(be,{children:a.jsx(tn,{title:"总群组数",value:(i==null?void 0:i.total_groups)??0,loading:n})})}),a.jsx(Ct,{xs:24,sm:12,lg:8,children:a.jsx(be,{children:a.jsx(tn,{title:"总会话数",value:(i==null?void 0:i.total_conversations)??0,loading:n})})}),a.jsx(Ct,{xs:24,sm:12,lg:8,children:a.jsx(be,{children:a.jsx(tn,{title:"总消息数",value:(i==null?void 0:i.total_messages)??0,loading:n})})}),a.jsx(Ct,{xs:24,sm:12,lg:8,children:a.jsx(be,{children:a.jsx(tn,{title:"今日消息",value:(i==null?void 0:i.today_messages)??0,loading:n})})})]}),a.jsxs(Es,{gutter:[16,16],children:[a.jsx(Ct,{xs:24,lg:12,children:a.jsx(be,{title:"用户增长趋势",loading:n,children:x(i==null?void 0:i.user_growth)})}),a.jsx(Ct,{xs:24,lg:12,children:a.jsx(be,{title:"消息增长趋势",loading:n,children:x(i==null?void 0:i.message_growth)})})]})]})},Fn={list:e=>ce.get("/admin/user-policy-groups",{params:e?{tenant_id:e}:void 0}),create:e=>ce.post("/admin/user-policy-groups",e),update:(e,t)=>ce.put(`/admin/user-policy-groups/${e}`,t),delete:(e,t)=>ce.delete(`/admin/user-policy-groups/${e}`,{params:t?{tenant_id:t}:void 0}),assignUsers:(e,t,r)=>ce.put(`/admin/user-policy-groups/${e}/users`,{tenant_id:t,user_ids:r}),getIPWhitelist:e=>ce.get("/admin/user-policy-groups/ip-whitelist",{params:e?{tenant_id:e}:void 0}),updateIPWhitelist:e=>ce.put("/admin/user-policy-groups/ip-whitelist",e)};function Yr(){const{tenants:e,tenantOptions:t,tenantMap:r,renderTenantId:n,fetchTenants:s}=G0();return g.useEffect(()=>{e.length===0&&s()},[]),{tenants:e,tenantOptions:t,tenantMap:r,renderTenantId:n,fetchTenants:s}}const ot=({showSearch:e=!0,placeholder:t="请选择租户",pageSize:r=50,...n})=>{const[s,i]=g.useState([]),[o,l]=g.useState(!1),c=g.useRef(),d=async h=>{l(!0);try{const m=await hn.list({keyword:(h==null?void 0:h.trim())||void 0,page:1,page_size:r});(m.code===0||m.code===200)&&m.data&&i(m.data.items.map(x=>({label:`${x.name} (${x.code})`,value:x.id})))}catch{}finally{l(!1)}},u=h=>{c.current&&clearTimeout(c.current),c.current=setTimeout(()=>{d(h)},300)};return g.useEffect(()=>(d(),()=>{c.current&&clearTimeout(c.current)}),[]),a.jsx($e,{showSearch:e,placeholder:t,filterOption:!1,loading:o,options:s,onSearch:u,...n})},Ke=e=>e?tm(e).format("YYYY-MM-DD HH:mm:ss"):"--",Y0={minLength:8,requireLetter:!0};function od(e){return`至少 ${e.minLength} 位,需包含数字${e.requireLetter?"和字母":""}`}function ld(e,t="请输入密码"){return[{required:!0,message:t},{min:e.minLength,message:`密码至少 ${e.minLength} 位`},{pattern:/^[\x21-\x7e]+$/,message:"只能包含英文字母、数字和英文特殊字符"},{pattern:/\d/,message:"必须包含数字"},...e.requireLetter?[{pattern:/[a-zA-Z]/,message:"必须包含字母"}]:[]]}async function R_(e){try{const t=await pa.getSettings(e?{tenant_id:e}:void 0);if((t.code===0||t.code===200)&&t.data)return{minLength:t.data.password_min_length||8,requireLetter:t.data.password_require_letter??!0}}catch{}return Y0}function Za(e=12){const t="ABCDEFGHIJKLMNOPQRSTUVWXYZ",r="abcdefghijklmnopqrstuvwxyz",n="0123456789",s=t+r+n,i=Math.max(e,12),o=[t[Math.floor(Math.random()*t.length)],r[Math.floor(Math.random()*r.length)],n[Math.floor(Math.random()*n.length)]];for(;o.length0;l--){const c=Math.floor(Math.random()*(l+1));[o[l],o[c]]=[o[c],o[l]]}return o.join("")}/*! xlsx.js (C) 2013-present SheetJS -- http://sheetjs.com */var ji={};ji.version="0.18.5";var q0=1252,D_=[874,932,936,949,950,1250,1251,1252,1253,1254,1255,1256,1257,1258,1e4],X0=function(e){D_.indexOf(e)!=-1&&(q0=e)};function P_(){X0(1252)}var ga=function(e){X0(e)};function N_(){ga(1200),P_()}var Qa=function(t){return String.fromCharCode(t)},cd=function(t){return String.fromCharCode(t)},Ii,Rn="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";function _a(e){for(var t="",r=0,n=0,s=0,i=0,o=0,l=0,c=0,d=0;d>2,n=e.charCodeAt(d++),o=(r&3)<<4|n>>4,s=e.charCodeAt(d++),l=(n&15)<<2|s>>6,c=s&63,isNaN(n)?l=c=64:isNaN(s)&&(c=64),t+=Rn.charAt(i)+Rn.charAt(o)+Rn.charAt(l)+Rn.charAt(c);return t}function vn(e){var t="",r=0,n=0,s=0,i=0,o=0,l=0,c=0;e=e.replace(/[^\w\+\/\=]/g,"");for(var d=0;d>4,t+=String.fromCharCode(r),l=Rn.indexOf(e.charAt(d++)),n=(o&15)<<4|l>>2,l!==64&&(t+=String.fromCharCode(n)),c=Rn.indexOf(e.charAt(d++)),s=(l&3)<<6|c,c!==64&&(t+=String.fromCharCode(s));return t}var ct=function(){return typeof Buffer<"u"&&typeof process<"u"&&typeof process.versions<"u"&&!!process.versions.node}(),wn=function(){if(typeof Buffer<"u"){var e=!Buffer.from;if(!e)try{Buffer.from("foo","utf8")}catch{e=!0}return e?function(t,r){return r?new Buffer(t,r):new Buffer(t)}:Buffer.from.bind(Buffer)}return function(){}}();function Jn(e){return ct?Buffer.alloc?Buffer.alloc(e):new Buffer(e):typeof Uint8Array<"u"?new Uint8Array(e):new Array(e)}function dd(e){return ct?Buffer.allocUnsafe?Buffer.allocUnsafe(e):new Buffer(e):typeof Uint8Array<"u"?new Uint8Array(e):new Array(e)}var Wr=function(t){return ct?wn(t,"binary"):t.split("").map(function(r){return r.charCodeAt(0)&255})};function oo(e){if(typeof ArrayBuffer>"u")return Wr(e);for(var t=new ArrayBuffer(e.length),r=new Uint8Array(t),n=0;n!=e.length;++n)r[n]=e.charCodeAt(n)&255;return t}function Da(e){if(Array.isArray(e))return e.map(function(n){return String.fromCharCode(n)}).join("");for(var t=[],r=0;r"u")throw new Error("Unsupported");return new Uint8Array(e)}var Yt=ct?function(e){return Buffer.concat(e.map(function(t){return Buffer.isBuffer(t)?t:wn(t)}))}:function(e){if(typeof Uint8Array<"u"){var t=0,r=0;for(t=0;t>6&31,s[r++]=128|o&63;else if(o>=55296&&o<57344){o=(o&1023)+64;var l=e.charCodeAt(++i)&1023;s[r++]=240|o>>8&7,s[r++]=128|o>>2&63,s[r++]=128|l>>6&15|(o&3)<<4,s[r++]=128|l&63}else s[r++]=224|o>>12&15,s[r++]=128|o>>6&63,s[r++]=128|o&63;r>n&&(t.push(s.slice(0,r)),r=0,s=Jn(65535),n=65530)}return t.push(s.slice(0,r)),Yt(t)}var ra=/\u0000/g,ei=/[\u0001-\u0006]/g;function ws(e){for(var t="",r=e.length-1;r>=0;)t+=e.charAt(r--);return t}function Hr(e,t){var r=""+e;return r.length>=t?r:Et("0",t-r.length)+r}function Pl(e,t){var r=""+e;return r.length>=t?r:Et(" ",t-r.length)+r}function Ci(e,t){var r=""+e;return r.length>=t?r:r+Et(" ",t-r.length)}function B_(e,t){var r=""+Math.round(e);return r.length>=t?r:Et("0",t-r.length)+r}function U_(e,t){var r=""+e;return r.length>=t?r:Et("0",t-r.length)+r}var ud=Math.pow(2,32);function us(e,t){if(e>ud||e<-ud)return B_(e,t);var r=Math.round(e);return U_(r,t)}function Ai(e,t){return t=t||0,e.length>=7+t&&(e.charCodeAt(t)|32)===103&&(e.charCodeAt(t+1)|32)===101&&(e.charCodeAt(t+2)|32)===110&&(e.charCodeAt(t+3)|32)===101&&(e.charCodeAt(t+4)|32)===114&&(e.charCodeAt(t+5)|32)===97&&(e.charCodeAt(t+6)|32)===108}var fd=[["Sun","Sunday"],["Mon","Monday"],["Tue","Tuesday"],["Wed","Wednesday"],["Thu","Thursday"],["Fri","Friday"],["Sat","Saturday"]],Do=[["J","Jan","January"],["F","Feb","February"],["M","Mar","March"],["A","Apr","April"],["M","May","May"],["J","Jun","June"],["J","Jul","July"],["A","Aug","August"],["S","Sep","September"],["O","Oct","October"],["N","Nov","November"],["D","Dec","December"]];function z_(e){return e||(e={}),e[0]="General",e[1]="0",e[2]="0.00",e[3]="#,##0",e[4]="#,##0.00",e[9]="0%",e[10]="0.00%",e[11]="0.00E+00",e[12]="# ?/?",e[13]="# ??/??",e[14]="m/d/yy",e[15]="d-mmm-yy",e[16]="d-mmm",e[17]="mmm-yy",e[18]="h:mm AM/PM",e[19]="h:mm:ss AM/PM",e[20]="h:mm",e[21]="h:mm:ss",e[22]="m/d/yy h:mm",e[37]="#,##0 ;(#,##0)",e[38]="#,##0 ;[Red](#,##0)",e[39]="#,##0.00;(#,##0.00)",e[40]="#,##0.00;[Red](#,##0.00)",e[45]="mm:ss",e[46]="[h]:mm:ss",e[47]="mmss.0",e[48]="##0.0E+0",e[49]="@",e[56]='"上午/下午 "hh"時"mm"分"ss"秒 "',e}var jt={0:"General",1:"0",2:"0.00",3:"#,##0",4:"#,##0.00",9:"0%",10:"0.00%",11:"0.00E+00",12:"# ?/?",13:"# ??/??",14:"m/d/yy",15:"d-mmm-yy",16:"d-mmm",17:"mmm-yy",18:"h:mm AM/PM",19:"h:mm:ss AM/PM",20:"h:mm",21:"h:mm:ss",22:"m/d/yy h:mm",37:"#,##0 ;(#,##0)",38:"#,##0 ;[Red](#,##0)",39:"#,##0.00;(#,##0.00)",40:"#,##0.00;[Red](#,##0.00)",45:"mm:ss",46:"[h]:mm:ss",47:"mmss.0",48:"##0.0E+0",49:"@",56:'"上午/下午 "hh"時"mm"分"ss"秒 "'},hd={5:37,6:38,7:39,8:40,23:0,24:0,25:0,26:0,27:14,28:14,29:14,30:14,31:14,50:14,51:14,52:14,53:14,54:14,55:14,56:14,57:14,58:14,59:1,60:2,61:3,62:4,67:9,68:10,69:12,70:13,71:14,72:14,73:15,74:16,75:17,76:20,77:21,78:22,79:45,80:46,81:47,82:0},$_={5:'"$"#,##0_);\\("$"#,##0\\)',63:'"$"#,##0_);\\("$"#,##0\\)',6:'"$"#,##0_);[Red]\\("$"#,##0\\)',64:'"$"#,##0_);[Red]\\("$"#,##0\\)',7:'"$"#,##0.00_);\\("$"#,##0.00\\)',65:'"$"#,##0.00_);\\("$"#,##0.00\\)',8:'"$"#,##0.00_);[Red]\\("$"#,##0.00\\)',66:'"$"#,##0.00_);[Red]\\("$"#,##0.00\\)',41:'_(* #,##0_);_(* \\(#,##0\\);_(* "-"_);_(@_)',42:'_("$"* #,##0_);_("$"* \\(#,##0\\);_("$"* "-"_);_(@_)',43:'_(* #,##0.00_);_(* \\(#,##0.00\\);_(* "-"??_);_(@_)',44:'_("$"* #,##0.00_);_("$"* \\(#,##0.00\\);_("$"* "-"??_);_(@_)'};function ki(e,t,r){for(var n=e<0?-1:1,s=e*n,i=0,o=1,l=0,c=1,d=0,u=0,h=Math.floor(s);dt&&(d>t?(u=c,l=i):(u=d,l=o)),!r)return[0,n*l,u];var m=Math.floor(n*l/u);return[m,n*l-m*u,u]}function ti(e,t,r){if(e>2958465||e<0)return null;var n=e|0,s=Math.floor(86400*(e-n)),i=0,o=[],l={D:n,T:s,u:86400*(e-n)-s,y:0,m:0,d:0,H:0,M:0,S:0,q:0};if(Math.abs(l.u)<1e-6&&(l.u=0),t&&t.date1904&&(n+=1462),l.u>.9999&&(l.u=0,++s==86400&&(l.T=s=0,++n,++l.D)),n===60)o=r?[1317,10,29]:[1900,2,29],i=3;else if(n===0)o=r?[1317,8,29]:[1900,1,0],i=6;else{n>60&&--n;var c=new Date(1900,0,1);c.setDate(c.getDate()+n-1),o=[c.getFullYear(),c.getMonth()+1,c.getDate()],i=c.getDay(),n<60&&(i=(i+6)%7),r&&(i=q_(c,o))}return l.y=o[0],l.m=o[1],l.d=o[2],l.S=s%60,s=Math.floor(s/60),l.M=s%60,s=Math.floor(s/60),l.H=s,l.q=i,l}var J0=new Date(1899,11,31,0,0,0),W_=J0.getTime(),H_=new Date(1900,2,1,0,0,0);function Z0(e,t){var r=e.getTime();return t?r-=1461*24*60*60*1e3:e>=H_&&(r+=24*60*60*1e3),(r-(W_+(e.getTimezoneOffset()-J0.getTimezoneOffset())*6e4))/(24*60*60*1e3)}function Nl(e){return e.indexOf(".")==-1?e:e.replace(/(?:\.0*|(\.\d*[1-9])0+)$/,"$1")}function V_(e){return e.indexOf("E")==-1?e:e.replace(/(?:\.0*|(\.\d*[1-9])0+)[Ee]/,"$1E").replace(/(E[+-])(\d)$/,"$10$2")}function K_(e){var t=e<0?12:11,r=Nl(e.toFixed(12));return r.length<=t||(r=e.toPrecision(10),r.length<=t)?r:e.toExponential(5)}function G_(e){var t=Nl(e.toFixed(11));return t.length>(e<0?12:11)||t==="0"||t==="-0"?e.toPrecision(6):t}function Y_(e){var t=Math.floor(Math.log(Math.abs(e))*Math.LOG10E),r;return t>=-4&&t<=-1?r=e.toPrecision(10+t):Math.abs(t)<=9?r=K_(e):t===10?r=e.toFixed(10).substr(0,12):r=G_(e),Nl(V_(r.toUpperCase()))}function ul(e,t){switch(typeof e){case"string":return e;case"boolean":return e?"TRUE":"FALSE";case"number":return(e|0)===e?e.toString(10):Y_(e);case"undefined":return"";case"object":if(e==null)return"";if(e instanceof Date)return Pn(14,Z0(e,t&&t.date1904),t)}throw new Error("unsupported value in General format: "+e)}function q_(e,t){t[0]-=581;var r=e.getDay();return e<60&&(r=(r+6)%7),r}function X_(e,t,r,n){var s="",i=0,o=0,l=r.y,c,d=0;switch(e){case 98:l=r.y+543;case 121:switch(t.length){case 1:case 2:c=l%100,d=2;break;default:c=l%1e4,d=4;break}break;case 109:switch(t.length){case 1:case 2:c=r.m,d=t.length;break;case 3:return Do[r.m-1][1];case 5:return Do[r.m-1][0];default:return Do[r.m-1][2]}break;case 100:switch(t.length){case 1:case 2:c=r.d,d=t.length;break;case 3:return fd[r.q][0];default:return fd[r.q][1]}break;case 104:switch(t.length){case 1:case 2:c=1+(r.H+11)%12,d=t.length;break;default:throw"bad hour format: "+t}break;case 72:switch(t.length){case 1:case 2:c=r.H,d=t.length;break;default:throw"bad hour format: "+t}break;case 77:switch(t.length){case 1:case 2:c=r.M,d=t.length;break;default:throw"bad minute format: "+t}break;case 115:if(t!="s"&&t!="ss"&&t!=".0"&&t!=".00"&&t!=".000")throw"bad second format: "+t;return r.u===0&&(t=="s"||t=="ss")?Hr(r.S,t.length):(n>=2?o=n===3?1e3:100:o=n===1?10:1,i=Math.round(o*(r.S+r.u)),i>=60*o&&(i=0),t==="s"?i===0?"0":""+i/o:(s=Hr(i,2+n),t==="ss"?s.substr(0,2):"."+s.substr(2,t.length-1)));case 90:switch(t){case"[h]":case"[hh]":c=r.D*24+r.H;break;case"[m]":case"[mm]":c=(r.D*24+r.H)*60+r.M;break;case"[s]":case"[ss]":c=((r.D*24+r.H)*60+r.M)*60+Math.round(r.S+r.u);break;default:throw"bad abstime format: "+t}d=t.length===3?1:2;break;case 101:c=l,d=1;break}var u=d>0?Hr(c,d):"";return u}function Dn(e){var t=3;if(e.length<=t)return e;for(var r=e.length%t,n=e.substr(0,r);r!=e.length;r+=t)n+=(n.length>0?",":"")+e.substr(r,t);return n}var Q0=/%/g;function J_(e,t,r){var n=t.replace(Q0,""),s=t.length-n.length;return xn(e,n,r*Math.pow(10,2*s))+Et("%",s)}function Z_(e,t,r){for(var n=t.length-1;t.charCodeAt(n-1)===44;)--n;return xn(e,t.substr(0,n),r/Math.pow(10,3*(t.length-n)))}function ef(e,t){var r,n=e.indexOf("E")-e.indexOf(".")-1;if(e.match(/^#+0.0E\+0$/)){if(t==0)return"0.0E+0";if(t<0)return"-"+ef(e,-t);var s=e.indexOf(".");s===-1&&(s=e.indexOf("E"));var i=Math.floor(Math.log(t)*Math.LOG10E)%s;if(i<0&&(i+=s),r=(t/Math.pow(10,i)).toPrecision(n+1+(s+i)%s),r.indexOf("e")===-1){var o=Math.floor(Math.log(t)*Math.LOG10E);for(r.indexOf(".")===-1?r=r.charAt(0)+"."+r.substr(1)+"E+"+(o-r.length+i):r+="E+"+(o-i);r.substr(0,2)==="0.";)r=r.charAt(0)+r.substr(2,s)+"."+r.substr(2+s),r=r.replace(/^0+([1-9])/,"$1").replace(/^0+\./,"0.");r=r.replace(/\+-/,"-")}r=r.replace(/^([+-]?)(\d*)\.(\d*)[Ee]/,function(l,c,d,u){return c+d+u.substr(0,(s+i)%s)+"."+u.substr(i)+"E"})}else r=t.toExponential(n);return e.match(/E\+00$/)&&r.match(/e[+-]\d$/)&&(r=r.substr(0,r.length-1)+"0"+r.charAt(r.length-1)),e.match(/E\-/)&&r.match(/e\+/)&&(r=r.replace(/e\+/,"e")),r.replace("e","E")}var tf=/# (\?+)( ?)\/( ?)(\d+)/;function Q_(e,t,r){var n=parseInt(e[4],10),s=Math.round(t*n),i=Math.floor(s/n),o=s-i*n,l=n;return r+(i===0?"":""+i)+" "+(o===0?Et(" ",e[1].length+1+e[4].length):Pl(o,e[1].length)+e[2]+"/"+e[3]+Hr(l,e[4].length))}function ev(e,t,r){return r+(t===0?"":""+t)+Et(" ",e[1].length+2+e[4].length)}var rf=/^#*0*\.([0#]+)/,nf=/\).*[0#]/,sf=/\(###\) ###\\?-####/;function ir(e){for(var t="",r,n=0;n!=e.length;++n)switch(r=e.charCodeAt(n)){case 35:break;case 63:t+=" ";break;case 48:t+="0";break;default:t+=String.fromCharCode(r)}return t}function md(e,t){var r=Math.pow(10,t);return""+Math.round(e*r)/r}function xd(e,t){var r=e-Math.floor(e),n=Math.pow(10,t);return t<(""+Math.round(r*n)).length?0:Math.round(r*n)}function tv(e,t){return t<(""+Math.round((e-Math.floor(e))*Math.pow(10,t))).length?1:0}function rv(e){return e<2147483647&&e>-2147483648?""+(e>=0?e|0:e-1|0):""+Math.floor(e)}function Dr(e,t,r){if(e.charCodeAt(0)===40&&!t.match(nf)){var n=t.replace(/\( */,"").replace(/ \)/,"").replace(/\)/,"");return r>=0?Dr("n",n,r):"("+Dr("n",n,-r)+")"}if(t.charCodeAt(t.length-1)===44)return Z_(e,t,r);if(t.indexOf("%")!==-1)return J_(e,t,r);if(t.indexOf("E")!==-1)return ef(t,r);if(t.charCodeAt(0)===36)return"$"+Dr(e,t.substr(t.charAt(1)==" "?2:1),r);var s,i,o,l,c=Math.abs(r),d=r<0?"-":"";if(t.match(/^00+$/))return d+us(c,t.length);if(t.match(/^[#?]+$/))return s=us(r,0),s==="0"&&(s=""),s.length>t.length?s:ir(t.substr(0,t.length-s.length))+s;if(i=t.match(tf))return Q_(i,c,d);if(t.match(/^#+0+$/))return d+us(c,t.length-t.indexOf("0"));if(i=t.match(rf))return s=md(r,i[1].length).replace(/^([^\.]+)$/,"$1."+ir(i[1])).replace(/\.$/,"."+ir(i[1])).replace(/\.(\d*)$/,function(x,f){return"."+f+Et("0",ir(i[1]).length-f.length)}),t.indexOf("0.")!==-1?s:s.replace(/^0\./,".");if(t=t.replace(/^#+([0.])/,"$1"),i=t.match(/^(0*)\.(#*)$/))return d+md(c,i[2].length).replace(/\.(\d*[1-9])0*$/,".$1").replace(/^(-?\d*)$/,"$1.").replace(/^0\./,i[1].length?"0.":".");if(i=t.match(/^#{1,3},##0(\.?)$/))return d+Dn(us(c,0));if(i=t.match(/^#,##0\.([#0]*0)$/))return r<0?"-"+Dr(e,t,-r):Dn(""+(Math.floor(r)+tv(r,i[1].length)))+"."+Hr(xd(r,i[1].length),i[1].length);if(i=t.match(/^#,#*,#0/))return Dr(e,t.replace(/^#,#*,/,""),r);if(i=t.match(/^([0#]+)(\\?-([0#]+))+$/))return s=ws(Dr(e,t.replace(/[\\-]/g,""),r)),o=0,ws(ws(t.replace(/\\/g,"")).replace(/[0#]/g,function(x){return o=0?Qr("n",n,r):"("+Qr("n",n,-r)+")"}if(t.charCodeAt(t.length-1)===44)return nv(e,t,r);if(t.indexOf("%")!==-1)return sv(e,t,r);if(t.indexOf("E")!==-1)return af(t,r);if(t.charCodeAt(0)===36)return"$"+Qr(e,t.substr(t.charAt(1)==" "?2:1),r);var s,i,o,l,c=Math.abs(r),d=r<0?"-":"";if(t.match(/^00+$/))return d+Hr(c,t.length);if(t.match(/^[#?]+$/))return s=""+r,r===0&&(s=""),s.length>t.length?s:ir(t.substr(0,t.length-s.length))+s;if(i=t.match(tf))return ev(i,c,d);if(t.match(/^#+0+$/))return d+Hr(c,t.length-t.indexOf("0"));if(i=t.match(rf))return s=(""+r).replace(/^([^\.]+)$/,"$1."+ir(i[1])).replace(/\.$/,"."+ir(i[1])),s=s.replace(/\.(\d*)$/,function(x,f){return"."+f+Et("0",ir(i[1]).length-f.length)}),t.indexOf("0.")!==-1?s:s.replace(/^0\./,".");if(t=t.replace(/^#+([0.])/,"$1"),i=t.match(/^(0*)\.(#*)$/))return d+(""+c).replace(/\.(\d*[1-9])0*$/,".$1").replace(/^(-?\d*)$/,"$1.").replace(/^0\./,i[1].length?"0.":".");if(i=t.match(/^#{1,3},##0(\.?)$/))return d+Dn(""+c);if(i=t.match(/^#,##0\.([#0]*0)$/))return r<0?"-"+Qr(e,t,-r):Dn(""+r)+"."+Et("0",i[1].length);if(i=t.match(/^#,#*,#0/))return Qr(e,t.replace(/^#,#*,/,""),r);if(i=t.match(/^([0#]+)(\\?-([0#]+))+$/))return s=ws(Qr(e,t.replace(/[\\-]/g,""),r)),o=0,ws(ws(t.replace(/\\/g,"")).replace(/[0#]/g,function(x){return o-1||r=="\\"&&e.charAt(t+1)=="-"&&"0#".indexOf(e.charAt(t+2))>-1););break;case"?":for(;e.charAt(++t)===r;);break;case"*":++t,(e.charAt(t)==" "||e.charAt(t)=="*")&&++t;break;case"(":case")":++t;break;case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":for(;t-1;);break;case" ":++t;break;default:++t;break}return!1}function iv(e,t,r,n){for(var s=[],i="",o=0,l="",c="t",d,u,h,m="H";o=12?"P":"A"),f.t="T",m="h",o+=3):e.substr(o,5).toUpperCase()==="AM/PM"?(d!=null&&(f.v=d.H>=12?"PM":"AM"),f.t="T",o+=5,m="h"):e.substr(o,5).toUpperCase()==="上午/下午"?(d!=null&&(f.v=d.H>=12?"下午":"上午"),f.t="T",o+=5,m="h"):(f.t="t",++o),d==null&&f.t==="T")return"";s[s.length]=f,c=l;break;case"[":for(i=l;e.charAt(o++)!=="]"&&o-1&&(i=(i.match(/\$([^-\[\]]*)/)||[])[1]||"$",lf(e)||(s[s.length]={t:"t",v:i}));break;case".":if(d!=null){for(i=l;++o-1;)i+=l;s[s.length]={t:"n",v:i};break;case"?":for(i=l;e.charAt(++o)===l;)i+=l;s[s.length]={t:l,v:i},c=l;break;case"*":++o,(e.charAt(o)==" "||e.charAt(o)=="*")&&++o;break;case"(":case")":s[s.length]={t:n===1?"t":l,v:l},++o;break;case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":for(i=l;o-1;)i+=e.charAt(o);s[s.length]={t:"D",v:i};break;case" ":s[s.length]={t:l,v:l},++o;break;case"$":s[s.length]={t:"t",v:"$"},++o;break;default:if(",$-+/():!^&'~{}<>=€acfijklopqrtuvwxzP".indexOf(l)===-1)throw new Error("unrecognized character "+l+" in "+e);s[s.length]={t:"t",v:l},++o;break}var p=0,I=0,S;for(o=s.length-1,c="t";o>=0;--o)switch(s[o].t){case"h":case"H":s[o].t=m,c="h",p<1&&(p=1);break;case"s":(S=s[o].v.match(/\.0+$/))&&(I=Math.max(I,S[0].length-1)),p<3&&(p=3);case"d":case"y":case"M":case"e":c=s[o].t;break;case"m":c==="s"&&(s[o].t="M",p<2&&(p=2));break;case"X":break;case"Z":p<1&&s[o].v.match(/[Hh]/)&&(p=1),p<2&&s[o].v.match(/[Mm]/)&&(p=2),p<3&&s[o].v.match(/[Ss]/)&&(p=3)}switch(p){case 0:break;case 1:d.u>=.5&&(d.u=0,++d.S),d.S>=60&&(d.S=0,++d.M),d.M>=60&&(d.M=0,++d.H);break;case 2:d.u>=.5&&(d.u=0,++d.S),d.S>=60&&(d.S=0,++d.M);break}var w="",v;for(o=0;o0){w.charCodeAt(0)==40?(R=t<0&&w.charCodeAt(0)===45?-t:t,j=xn("n",w,R)):(R=t<0&&n>1?-t:t,j=xn("n",w,R),R<0&&s[0]&&s[0].t=="t"&&(j=j.substr(1),s[0].v="-"+s[0].v)),v=j.length-1;var T=s.length;for(o=0;o-1){T=o;break}var C=s.length;if(T===s.length&&j.indexOf("E")===-1){for(o=s.length-1;o>=0;--o)s[o]==null||"n?".indexOf(s[o].t)===-1||(v>=s[o].v.length-1?(v-=s[o].v.length,s[o].v=j.substr(v+1,s[o].v.length)):v<0?s[o].v="":(s[o].v=j.substr(0,v+1),v=-1),s[o].t="t",C=o);v>=0&&C=0;--o)if(!(s[o]==null||"n?".indexOf(s[o].t)===-1)){for(u=s[o].v.indexOf(".")>-1&&o===T?s[o].v.indexOf(".")-1:s[o].v.length-1,y=s[o].v.substr(u+1);u>=0;--u)v>=0&&(s[o].v.charAt(u)==="0"||s[o].v.charAt(u)==="#")&&(y=j.charAt(v--)+y);s[o].v=y,s[o].t="t",C=o}for(v>=0&&C-1&&o===T?s[o].v.indexOf(".")+1:0,y=s[o].v.substr(0,u);u-1&&(R=n>1&&t<0&&o>0&&s[o-1].v==="-"?-t:t,s[o].v=xn(s[o].t,s[o].v,R),s[o].t="t");var F="";for(o=0;o!==s.length;++o)s[o]!=null&&(F+=s[o].v);return F}var pd=/\[(=|>[=]?|<[>=]?)(-?\d+(?:\.\d*)?)\]/;function gd(e,t){if(t==null)return!1;var r=parseFloat(t[2]);switch(t[1]){case"=":if(e==r)return!0;break;case">":if(e>r)return!0;break;case"<":if(e":if(e!=r)return!0;break;case">=":if(e>=r)return!0;break;case"<=":if(e<=r)return!0;break}return!1}function ov(e,t){var r=av(e),n=r.length,s=r[n-1].indexOf("@");if(n<4&&s>-1&&--n,r.length>4)throw new Error("cannot find right format for |"+r.join("|")+"|");if(typeof t!="number")return[4,r.length===4||s>-1?r[r.length-1]:"@"];switch(r.length){case 1:r=s>-1?["General","General","General",r[0]]:[r[0],r[0],r[0],"@"];break;case 2:r=s>-1?[r[0],r[0],r[0],r[1]]:[r[0],r[1],r[0],"@"];break;case 3:r=s>-1?[r[0],r[1],r[0],r[2]]:[r[0],r[1],r[2],"@"];break}var i=t>0?r[0]:t<0?r[1]:r[2];if(r[0].indexOf("[")===-1&&r[1].indexOf("[")===-1)return[n,i];if(r[0].match(/\[[=<>]/)!=null||r[1].match(/\[[=<>]/)!=null){var o=r[0].match(pd),l=r[1].match(pd);return gd(t,o)?[n,r[0]]:gd(t,l)?[n,r[1]]:[n,r[o!=null&&l!=null?2:1]]}return[n,i]}function Pn(e,t,r){r==null&&(r={});var n="";switch(typeof e){case"string":e=="m/d/yy"&&r.dateNF?n=r.dateNF:n=e;break;case"number":e==14&&r.dateNF?n=r.dateNF:n=(r.table!=null?r.table:jt)[e],n==null&&(n=r.table&&r.table[hd[e]]||jt[hd[e]]),n==null&&(n=$_[e]||"General");break}if(Ai(n,0))return ul(t,r);t instanceof Date&&(t=Z0(t,r.date1904));var s=ov(n,t);if(Ai(s[1]))return ul(t,r);if(t===!0)t="TRUE";else if(t===!1)t="FALSE";else if(t===""||t==null)return"";return iv(s[1],t,r,s[0])}function cf(e,t){if(typeof t!="number"){t=+t||-1;for(var r=0;r<392;++r){if(jt[r]==null){t<0&&(t=r);continue}if(jt[r]==e){t=r;break}}t<0&&(t=391)}return jt[t]=e,t}function lo(e){for(var t=0;t!=392;++t)e[t]!==void 0&&cf(e[t],t)}function co(){jt=z_()}var df=/[dD]+|[mM]+|[yYeE]+|[Hh]+|[Ss]+/g;function lv(e){var t=typeof e=="number"?jt[e]:e;return t=t.replace(df,"(\\d+)"),new RegExp("^"+t+"$")}function cv(e,t,r){var n=-1,s=-1,i=-1,o=-1,l=-1,c=-1;(t.match(df)||[]).forEach(function(h,m){var _=parseInt(r[m+1],10);switch(h.toLowerCase().charAt(0)){case"y":n=_;break;case"d":i=_;break;case"h":o=_;break;case"s":c=_;break;case"m":o>=0?l=_:s=_;break}}),c>=0&&l==-1&&s>=0&&(l=s,s=-1);var d=(""+(n>=0?n:new Date().getFullYear())).slice(-4)+"-"+("00"+(s>=1?s:1)).slice(-2)+"-"+("00"+(i>=1?i:1)).slice(-2);d.length==7&&(d="0"+d),d.length==8&&(d="20"+d);var u=("00"+(o>=0?o:0)).slice(-2)+":"+("00"+(l>=0?l:0)).slice(-2)+":"+("00"+(c>=0?c:0)).slice(-2);return o==-1&&l==-1&&c==-1?d:n==-1&&s==-1&&i==-1?u:d+"T"+u}var dv=function(){var e={};e.version="1.2.0";function t(){for(var j=0,T=new Array(256),C=0;C!=256;++C)j=C,j=j&1?-306674912^j>>>1:j>>>1,j=j&1?-306674912^j>>>1:j>>>1,j=j&1?-306674912^j>>>1:j>>>1,j=j&1?-306674912^j>>>1:j>>>1,j=j&1?-306674912^j>>>1:j>>>1,j=j&1?-306674912^j>>>1:j>>>1,j=j&1?-306674912^j>>>1:j>>>1,j=j&1?-306674912^j>>>1:j>>>1,T[C]=j;return typeof Int32Array<"u"?new Int32Array(T):T}var r=t();function n(j){var T=0,C=0,F=0,P=typeof Int32Array<"u"?new Int32Array(4096):new Array(4096);for(F=0;F!=256;++F)P[F]=j[F];for(F=0;F!=256;++F)for(C=j[F],T=256+F;T<4096;T+=256)C=P[T]=C>>>8^j[C&255];var V=[];for(F=1;F!=16;++F)V[F-1]=typeof Int32Array<"u"?P.subarray(F*256,F*256+256):P.slice(F*256,F*256+256);return V}var s=n(r),i=s[0],o=s[1],l=s[2],c=s[3],d=s[4],u=s[5],h=s[6],m=s[7],_=s[8],x=s[9],f=s[10],p=s[11],I=s[12],S=s[13],w=s[14];function v(j,T){for(var C=T^-1,F=0,P=j.length;F>>8^r[(C^j.charCodeAt(F++))&255];return~C}function y(j,T){for(var C=T^-1,F=j.length-15,P=0;P>8&255]^I[j[P++]^C>>16&255]^p[j[P++]^C>>>24]^f[j[P++]]^x[j[P++]]^_[j[P++]]^m[j[P++]]^h[j[P++]]^u[j[P++]]^d[j[P++]]^c[j[P++]]^l[j[P++]]^o[j[P++]]^i[j[P++]]^r[j[P++]];for(F+=15;P>>8^r[(C^j[P++])&255];return~C}function R(j,T){for(var C=T^-1,F=0,P=j.length,V=0,X=0;F>>8^r[(C^V)&255]:V<2048?(C=C>>>8^r[(C^(192|V>>6&31))&255],C=C>>>8^r[(C^(128|V&63))&255]):V>=55296&&V<57344?(V=(V&1023)+64,X=j.charCodeAt(F++)&1023,C=C>>>8^r[(C^(240|V>>8&7))&255],C=C>>>8^r[(C^(128|V>>2&63))&255],C=C>>>8^r[(C^(128|X>>6&15|(V&3)<<4))&255],C=C>>>8^r[(C^(128|X&63))&255]):(C=C>>>8^r[(C^(224|V>>12&15))&255],C=C>>>8^r[(C^(128|V>>6&63))&255],C=C>>>8^r[(C^(128|V&63))&255]);return~C}return e.table=r,e.bstr=v,e.buf=y,e.str=R,e}(),xt=function(){var t={};t.version="1.2.1";function r(A,L){for(var O=A.split("/"),N=L.split("/"),M=0,U=0,ne=Math.min(O.length,N.length);M>>1,A.write_shift(2,O);var N=L.getFullYear()-1980;N=N<<4|L.getMonth()+1,N=N<<5|L.getDate(),A.write_shift(2,N)}function o(A){var L=A.read_shift(2)&65535,O=A.read_shift(2)&65535,N=new Date,M=O&31;O>>>=5;var U=O&15;O>>>=4,N.setMilliseconds(0),N.setFullYear(O+1980),N.setMonth(U-1),N.setDate(M);var ne=L&31;L>>>=5;var ge=L&63;return L>>>=6,N.setHours(L),N.setMinutes(ge),N.setSeconds(ne<<1),N}function l(A){Tr(A,0);for(var L={},O=0;A.l<=A.length-4;){var N=A.read_shift(2),M=A.read_shift(2),U=A.l+M,ne={};switch(N){case 21589:O=A.read_shift(1),O&1&&(ne.mtime=A.read_shift(4)),M>5&&(O&2&&(ne.atime=A.read_shift(4)),O&4&&(ne.ctime=A.read_shift(4))),ne.mtime&&(ne.mt=new Date(ne.mtime*1e3));break}A.l=U,L[N]=ne}return L}var c;function d(){return c||(c={})}function u(A,L){if(A[0]==80&&A[1]==75)return bn(A,L);if((A[0]|32)==109&&(A[1]|32)==105)return Xr(A,L);if(A.length<512)throw new Error("CFB file size "+A.length+" < 512");var O=3,N=512,M=0,U=0,ne=0,ge=0,re=0,ae=[],oe=A.slice(0,512);Tr(oe,0);var Ee=h(oe);switch(O=Ee[0],O){case 3:N=512;break;case 4:N=4096;break;case 0:if(Ee[1]==0)return bn(A,L);default:throw new Error("Major Version: Expected 3 or 4 saw "+O)}N!==512&&(oe=A.slice(0,N),Tr(oe,28));var Pe=A.slice(0,N);m(oe,O);var Be=oe.read_shift(4,"i");if(O===3&&Be!==0)throw new Error("# Directory Sectors: Expected 0 saw "+Be);oe.l+=4,ne=oe.read_shift(4,"i"),oe.l+=4,oe.chk("00100000","Mini Stream Cutoff Size: "),ge=oe.read_shift(4,"i"),M=oe.read_shift(4,"i"),re=oe.read_shift(4,"i"),U=oe.read_shift(4,"i");for(var Ae=-1,Le=0;Le<109&&(Ae=oe.read_shift(4,"i"),!(Ae<0));++Le)ae[Le]=Ae;var H=_(A,N);p(re,U,H,N,ae);var ee=S(H,ne,ae,N);ee[ne].name="!Directory",M>0&&ge!==X&&(ee[ge].name="!MiniFAT"),ee[ae[0]].name="!FAT",ee.fat_addrs=ae,ee.ssz=N;var ye={},Ne=[],We=[],dt=[];w(ne,ee,H,Ne,M,ye,We,ge),x(We,dt,Ne),Ne.shift();var yr={FileIndex:We,FullPaths:dt};return L&&L.raw&&(yr.raw={header:Pe,sectors:H}),yr}function h(A){if(A[A.l]==80&&A[A.l+1]==75)return[0,0];A.chk(_e,"Header Signature: "),A.l+=16;var L=A.read_shift(2,"u");return[A.read_shift(2,"u"),L]}function m(A,L){var O=9;switch(A.l+=2,O=A.read_shift(2)){case 9:if(L!=3)throw new Error("Sector Shift: Expected 9 saw "+O);break;case 12:if(L!=4)throw new Error("Sector Shift: Expected 12 saw "+O);break;default:throw new Error("Sector Shift: Expected 9 or 12 saw "+O)}A.chk("0600","Mini Sector Shift: "),A.chk("000000000000","Reserved: ")}function _(A,L){for(var O=Math.ceil(A.length/L)-1,N=[],M=1;M0&&ne>=0;)U.push(L.slice(ne*V,ne*V+V)),M-=V,ne=Hn(O,ne*4);return U.length===0?pe(0):Yt(U).slice(0,A.size)}function p(A,L,O,N,M){var U=X;if(A===X){if(L!==0)throw new Error("DIFAT chain shorter than expected")}else if(A!==-1){var ne=O[A],ge=(N>>>2)-1;if(!ne)return;for(var re=0;re=0;){M[re]=!0,U[U.length]=re,ne.push(A[re]);var oe=O[Math.floor(re*4/N)];if(ae=re*4&ge,N<4+ae)throw new Error("FAT boundary crossed: "+re+" 4 "+N);if(!A[oe])break;re=Hn(A[oe],ae)}return{nodes:U,data:Ed([ne])}}function S(A,L,O,N){var M=A.length,U=[],ne=[],ge=[],re=[],ae=N-1,oe=0,Ee=0,Pe=0,Be=0;for(oe=0;oe=M&&(Pe-=M),!ne[Pe]){re=[];var Ae=[];for(Ee=Pe;Ee>=0;){Ae[Ee]=!0,ne[Ee]=!0,ge[ge.length]=Ee,re.push(A[Ee]);var Le=O[Math.floor(Ee*4/N)];if(Be=Ee*4&ae,N<4+Be)throw new Error("FAT boundary crossed: "+Ee+" 4 "+N);if(!A[Le]||(Ee=Hn(A[Le],Be),Ae[Ee]))break}U[Pe]={nodes:ge,data:Ed([re])}}return U}function w(A,L,O,N,M,U,ne,ge){for(var re=0,ae=N.length?2:0,oe=L[A].data,Ee=0,Pe=0,Be;Ee0&&re!==X&&(L[re].name="!StreamData")):Le.size>=4096?(Le.storage="fat",L[Le.start]===void 0&&(L[Le.start]=I(O,Le.start,L.fat_addrs,L.ssz)),L[Le.start].name=Le.name,Le.content=L[Le.start].data.slice(0,Le.size)):(Le.storage="minifat",Le.size<0?Le.size=0:re!==X&&Le.start!==X&&L[re]&&(Le.content=f(Le,L[re].data,(L[ge]||{}).data))),Le.content&&Tr(Le.content,0),U[Be]=Le,ne.push(Le)}}function v(A,L){return new Date((Er(A,L+4)/1e7*Math.pow(2,32)+Er(A,L)/1e7-11644473600)*1e3)}function y(A,L){return d(),u(c.readFileSync(A),L)}function R(A,L){var O=L&&L.type;switch(O||ct&&Buffer.isBuffer(A)&&(O="buffer"),O||"base64"){case"file":return y(A,L);case"base64":return u(Wr(vn(A)),L);case"binary":return u(Wr(A),L)}return u(A,L)}function j(A,L){var O=L||{},N=O.root||"Root Entry";if(A.FullPaths||(A.FullPaths=[]),A.FileIndex||(A.FileIndex=[]),A.FullPaths.length!==A.FileIndex.length)throw new Error("inconsistent CFB structure");A.FullPaths.length===0&&(A.FullPaths[0]=N+"/",A.FileIndex[0]={name:N,type:5}),O.CLSID&&(A.FileIndex[0].clsid=O.CLSID),T(A)}function T(A){var L="Sh33tJ5";if(!xt.find(A,"/"+L)){var O=pe(4);O[0]=55,O[1]=O[3]=50,O[2]=54,A.FileIndex.push({name:L,type:2,content:O,size:4,L:69,R:69,C:69}),A.FullPaths.push(A.FullPaths[0]+L),C(A)}}function C(A,L){j(A);for(var O=!1,N=!1,M=A.FullPaths.length-1;M>=0;--M){var U=A.FileIndex[M];switch(U.type){case 0:N?O=!0:(A.FileIndex.pop(),A.FullPaths.pop());break;case 1:case 2:case 5:N=!0,isNaN(U.R*U.L*U.C)&&(O=!0),U.R>-1&&U.L>-1&&U.R==U.L&&(O=!0);break;default:O=!0;break}}if(!(!O&&!L)){var ne=new Date(1987,1,19),ge=0,re=Object.create?Object.create(null):{},ae=[];for(M=0;M1?1:-1,Ee.size=0,Ee.type=5;else if(Pe.slice(-1)=="/"){for(ge=M+1;ge=ae.length?-1:ge,ge=M+1;ge=ae.length?-1:ge,Ee.type=1}else n(A.FullPaths[M+1]||"")==n(Pe)&&(Ee.R=M+1),Ee.type=2}}}function F(A,L){var O=L||{};if(O.fileType=="mad")return It(A,O);switch(C(A),O.fileType){case"zip":return St(A,O)}var N=function(Be){for(var Ae=0,Le=0,H=0;H0&&(ye<4096?Ae+=ye+63>>6:Le+=ye+511>>9)}}for(var Ne=Be.FullPaths.length+3>>2,We=Ae+7>>3,dt=Ae+127>>7,yr=We+Le+Ne+dt,bt=yr+127>>7,tr=bt<=109?0:Math.ceil((bt-109)/127);yr+bt+tr+127>>7>bt;)tr=++bt<=109?0:Math.ceil((bt-109)/127);var wt=[1,tr,bt,dt,Ne,Le,Ae,0];return Be.FileIndex[0].size=Ae<<6,wt[7]=(Be.FileIndex[0].start=wt[0]+wt[1]+wt[2]+wt[3]+wt[4]+wt[5])+(wt[6]+7>>3),wt}(A),M=pe(N[7]<<9),U=0,ne=0;{for(U=0;U<8;++U)M.write_shift(1,B[U]);for(U=0;U<8;++U)M.write_shift(2,0);for(M.write_shift(2,62),M.write_shift(2,3),M.write_shift(2,65534),M.write_shift(2,9),M.write_shift(2,6),U=0;U<3;++U)M.write_shift(2,0);for(M.write_shift(4,0),M.write_shift(4,N[2]),M.write_shift(4,N[0]+N[1]+N[2]+N[3]-1),M.write_shift(4,0),M.write_shift(4,4096),M.write_shift(4,N[3]?N[0]+N[1]+N[2]-1:X),M.write_shift(4,N[3]),M.write_shift(-4,N[1]?N[0]-1:X),M.write_shift(4,N[1]),U=0;U<109;++U)M.write_shift(-4,U>9)));for(ge(N[6]+7>>3);M.l&511;)M.write_shift(-4,J.ENDOFCHAIN);for(ne=U=0,re=0;re=4096)&&(oe.start=ne,ge(ae+63>>6)));for(;M.l&511;)M.write_shift(-4,J.ENDOFCHAIN);for(U=0;U=4096)if(M.l=oe.start+1<<9,ct&&Buffer.isBuffer(oe.content))oe.content.copy(M,M.l,0,oe.size),M.l+=oe.size+511&-512;else{for(re=0;re0&&oe.size<4096)if(ct&&Buffer.isBuffer(oe.content))oe.content.copy(M,M.l,0,oe.size),M.l+=oe.size+63&-64;else{for(re=0;re>16|L>>8|L)&255}for(var me=typeof Uint8Array<"u",ie=me?new Uint8Array(256):[],he=0;he<256;++he)ie[he]=Z(he);function we(A,L){var O=ie[A&255];return L<=8?O>>>8-L:(O=O<<8|ie[A>>8&255],L<=16?O>>>16-L:(O=O<<8|ie[A>>16&255],O>>>24-L))}function He(A,L){var O=L&7,N=L>>>3;return(A[N]|(O<=6?0:A[N+1]<<8))>>>O&3}function ze(A,L){var O=L&7,N=L>>>3;return(A[N]|(O<=5?0:A[N+1]<<8))>>>O&7}function gt(A,L){var O=L&7,N=L>>>3;return(A[N]|(O<=4?0:A[N+1]<<8))>>>O&15}function qe(A,L){var O=L&7,N=L>>>3;return(A[N]|(O<=3?0:A[N+1]<<8))>>>O&31}function le(A,L){var O=L&7,N=L>>>3;return(A[N]|(O<=1?0:A[N+1]<<8))>>>O&127}function fe(A,L,O){var N=L&7,M=L>>>3,U=(1<>>N;return O<8-N||(ne|=A[M+1]<<8-N,O<16-N)||(ne|=A[M+2]<<16-N,O<24-N)||(ne|=A[M+3]<<24-N),ne&U}function Re(A,L,O){var N=L&7,M=L>>>3;return N<=5?A[M]|=(O&7)<>8-N),L+3}function Ue(A,L,O){var N=L&7,M=L>>>3;return O=(O&1)<>>3;return O<<=N,A[M]|=O&255,O>>>=8,A[M+1]=O,L+8}function Tt(A,L,O){var N=L&7,M=L>>>3;return O<<=N,A[M]|=O&255,O>>>=8,A[M+1]=O&255,A[M+2]=O>>>8,L+16}function yt(A,L){var O=A.length,N=2*O>L?2*O:L+5,M=0;if(O>=L)return A;if(ct){var U=dd(N);if(A.copy)A.copy(U);else for(;M>N-Ee,ne=(1<=0;--ne)L[ge|ne<0;)re[re.l++]=ge[ae++]}return re.l}function ne(ge,re){for(var ae=0,oe=0,Ee=me?new Uint16Array(32768):[];oe0;)re[re.l++]=ge[oe++];ae=re.l*8;continue}ae=Re(re,ae,+(oe+Pe==ge.length)+2);for(var Be=0;Pe-- >0;){var Ae=ge[oe];Be=(Be<<5^Ae)&32767;var Le=-1,H=0;if((Le=Ee[Be])&&(Le|=oe&-32768,Le>oe&&(Le-=32768),Le2){Ae=M[H],Ae<=22?ae=nt(re,ae,ie[Ae+1]>>1)-1:(nt(re,ae,3),ae+=5,nt(re,ae,ie[Ae-23]>>5),ae+=3);var ee=Ae<8?0:Ae-4>>2;ee>0&&(Tt(re,ae,H-q[Ae]),ae+=ee),Ae=L[oe-Le],ae=nt(re,ae,ie[Ae]>>3),ae-=3;var ye=Ae<4?0:Ae-2>>1;ye>0&&(Tt(re,ae,oe-Le-K[Ae]),ae+=ye);for(var Ne=0;Ne>8-Ae;for(var Le=(1<<7-Ae)-1;Le>=0;--Le)Wt[Be|Le<>>=3){case 16:for(U=3+He(A,L),L+=2,Be=H[H.length-1];U-- >0;)H.push(Be);break;case 17:for(U=3+ze(A,L),L+=3;U-- >0;)H.push(0);break;case 18:for(U=11+le(A,L),L+=7;U-- >0;)H.push(0);break;default:H.push(Be),re>>0,ge=0,re=0;!(N&1);){if(N=ze(A,O),O+=3,N>>>1)N>>1==1?(ge=9,re=5):(O=as(A,O),ge=Sn,re=cr);else{O&7&&(O+=8-(O&7));var ae=A[O>>>3]|A[(O>>>3)+1]<<8;if(O+=32,ae>0)for(!L&&ne0;)M[U++]=A[O>>>3],O+=8;continue}for(;;){!L&&ne>>1==1?Qe[oe]:et[oe];if(O+=Ee&15,Ee>>>=4,!(Ee>>>8&255))M[U++]=Ee;else{if(Ee==256)break;Ee-=257;var Pe=Ee<8?0:Ee-4>>2;Pe>5&&(Pe=0);var Be=U+q[Ee];Pe>0&&(Be+=fe(A,O,Pe),O+=Pe),oe=fe(A,O,re),Ee=N>>>1==1?Xe[oe]:_t[oe],O+=Ee&15,Ee>>>=4;var Ae=Ee<4?0:Ee-2>>1,Le=K[Ee];for(Ae>0&&(Le+=fe(A,O,Ae),O+=Ae),!L&&ne>>3]:[M.slice(0,U),O+7>>>3]}function kr(A,L){var O=A.slice(A.l||0),N=Tn(O,L);return A.l+=N[1],N[0]}function qr(A,L){if(A)typeof console<"u"&&console.error(L);else throw new Error(L)}function bn(A,L){var O=A;Tr(O,0);var N=[],M=[],U={FileIndex:N,FullPaths:M};j(U,{root:L.root});for(var ne=O.length-4;(O[ne]!=80||O[ne+1]!=75||O[ne+2]!=5||O[ne+3]!=6)&&ne>=0;)--ne;O.l=ne+4,O.l+=4;var ge=O.read_shift(2);O.l+=6;var re=O.read_shift(4);for(O.l=re,ne=0;ne0&&(O=O.slice(0,O.length-1),O=O.slice(0,O.lastIndexOf("/")+1),U.slice(0,O.length)!=O););var ne=(N[1]||"").match(/boundary="(.*?)"/);if(!ne)throw new Error("MAD cannot find boundary");var ge="--"+(ne[1]||""),re=[],ae=[],oe={FileIndex:re,FullPaths:ae};j(oe);var Ee,Pe=0;for(M=0;M=32&&Be<128&&++Ee;var Le=Ee>=Pe*4/5;M.push(N),M.push("Content-Location: "+(O.root||"file:///C:/SheetJS/")+ne),M.push("Content-Transfer-Encoding: "+(Le?"quoted-printable":"base64")),M.push("Content-Type: "+kt(ge,ne)),M.push(""),M.push(Le?Qt(oe):dr(oe))}return M.push(N+`--\r +`),M.join(`\r +`)}function Fr(A){var L={};return j(L,A),L}function er(A,L,O,N){var M=N&&N.unsafe;M||j(A);var U=!M&&xt.find(A,L);if(!U){var ne=A.FullPaths[0];L.slice(0,ne.length)==ne?ne=L:(ne.slice(-1)!="/"&&(ne+="/"),ne=(ne+L).replace("//","/")),U={name:s(L),type:2},A.FileIndex.push(U),A.FullPaths.push(ne),M||xt.utils.cfb_gc(A)}return U.content=O,U.size=O?O.length:0,N&&(N.CLSID&&(U.clsid=N.CLSID),N.mt&&(U.mt=N.mt),N.ct&&(U.ct=N.ct)),U}function vr(A,L){j(A);var O=xt.find(A,L);if(O){for(var N=0;N0?r.setTime(r.getTime()+r.getTimezoneOffset()*60*1e3):t<0&&r.setTime(r.getTime()-r.getTimezoneOffset()*60*1e3),r;if(e instanceof Date)return e;if(hf.getFullYear()==1917&&!isNaN(r.getFullYear())){var n=r.getFullYear();return e.indexOf(""+n)>-1||r.setFullYear(r.getFullYear()+100),r}var s=e.match(/\d+/g)||["2017","2","19","0","0","0"],i=new Date(+s[0],+s[1]-1,+s[2],+s[3]||0,+s[4]||0,+s[5]||0);return e.indexOf("Z")>-1&&(i=new Date(i.getTime()-i.getTimezoneOffset()*60*1e3)),i}function fo(e,t){if(ct&&Buffer.isBuffer(e))return e.toString("binary");if(typeof TextDecoder<"u")try{var r={"€":"€","‚":"‚",ƒ:"ƒ","„":"„","…":"…","†":"†","‡":"‡","ˆ":"ˆ","‰":"‰",Š:"Š","‹":"‹",Œ:"Œ",Ž:"Ž","‘":"‘","’":"’","“":"“","”":"”","•":"•","–":"–","—":"—","˜":"˜","™":"™",š:"š","›":"›",œ:"œ",ž:"ž",Ÿ:"Ÿ"};return Array.isArray(e)&&(e=new Uint8Array(e)),new TextDecoder("latin1").decode(e).replace(/[€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ]/g,function(i){return r[i]||i})}catch{}for(var n=[],s=0;s!=e.length;++s)n.push(String.fromCharCode(e[s]));return n.join("")}function gr(e){if(typeof JSON<"u"&&!Array.isArray(e))return JSON.parse(JSON.stringify(e));if(typeof e!="object"||e==null)return e;if(e instanceof Date)return new Date(e.getTime());var t={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=gr(e[r]));return t}function Et(e,t){for(var r="";r.length3&&xv.indexOf(o)==-1)return r}else if(o.match(/[a-z]/))return r;return n<0||n>8099?r:(s>0||i>1)&&n!=101?t:e.match(/[^-0-9:,\/\\]/)?r:t}function Ze(e,t,r){if(e.FullPaths){if(typeof r=="string"){var n;return ct?n=wn(r):n=M_(r),xt.utils.cfb_add(e,t,n)}xt.utils.cfb_add(e,t,r)}else e.file(t,r)}function Ml(){return xt.utils.cfb_new()}var Rt=`\r +`,pv={""":'"',"'":"'",">":">","<":"<","&":"&"},Bl=Ll(pv),Ul=/[&<>'"]/g,gv=/[\u0000-\u0008\u000b-\u001f]/g;function ht(e){var t=e+"";return t.replace(Ul,function(r){return Bl[r]}).replace(gv,function(r){return"_x"+("000"+r.charCodeAt(0).toString(16)).slice(-4)+"_"})}function wd(e){return ht(e).replace(/ /g,"_x0020_")}var mf=/[\u0000-\u001f]/g;function _v(e){var t=e+"";return t.replace(Ul,function(r){return Bl[r]}).replace(/\n/g,"
").replace(mf,function(r){return"&#x"+("000"+r.charCodeAt(0).toString(16)).slice(-4)+";"})}function vv(e){var t=e+"";return t.replace(Ul,function(r){return Bl[r]}).replace(mf,function(r){return"&#x"+r.charCodeAt(0).toString(16).toUpperCase()+";"})}function yv(e){return e.replace(/(\r\n|[\r\n])/g," ")}function wv(e){switch(e){case 1:case!0:case"1":case"true":case"TRUE":return!0;default:return!1}}function Po(e){for(var t="",r=0,n=0,s=0,i=0,o=0,l=0;r191&&n<224){o=(n&31)<<6,o|=s&63,t+=String.fromCharCode(o);continue}if(i=e.charCodeAt(r++),n<240){t+=String.fromCharCode((n&15)<<12|(s&63)<<6|i&63);continue}o=e.charCodeAt(r++),l=((n&7)<<18|(s&63)<<12|(i&63)<<6|o&63)-65536,t+=String.fromCharCode(55296+(l>>>10&1023)),t+=String.fromCharCode(56320+(l&1023))}return t}function Sd(e){var t=Jn(2*e.length),r,n,s=1,i=0,o=0,l;for(n=0;n>>10&1023),r=56320+(r&1023)),o!==0&&(t[i++]=o&255,t[i++]=o>>>8,o=0),t[i++]=r%256,t[i++]=r>>>8;return t.slice(0,i).toString("ucs2")}function Td(e){return wn(e,"binary").toString("utf8")}var ri="foo bar baz☃🍣",na=ct&&(Td(ri)==Po(ri)&&Td||Sd(ri)==Po(ri)&&Sd)||Po,ya=ct?function(e){return wn(e,"utf8").toString("binary")}:function(e){for(var t=[],r=0,n=0,s=0;r>6))),t.push(String.fromCharCode(128+(n&63)));break;case(n>=55296&&n<57344):n-=55296,s=e.charCodeAt(r++)-56320+(n<<10),t.push(String.fromCharCode(240+(s>>18&7))),t.push(String.fromCharCode(144+(s>>12&63))),t.push(String.fromCharCode(128+(s>>6&63))),t.push(String.fromCharCode(128+(s&63)));break;default:t.push(String.fromCharCode(224+(n>>12))),t.push(String.fromCharCode(128+(n>>6&63))),t.push(String.fromCharCode(128+(n&63)))}return t.join("")},Sv=function(){var e=[["nbsp"," "],["middot","·"],["quot",'"'],["apos","'"],["gt",">"],["lt","<"],["amp","&"]].map(function(t){return[new RegExp("&"+t[0]+";","ig"),t[1]]});return function(r){for(var n=r.replace(/^[\t\n\r ]+/,"").replace(/[\t\n\r ]+$/,"").replace(/>\s+/g,">").replace(/\s+/g,` +`).replace(/<[^>]*>/g,""),s=0;s"+t+""}function wa(e){return Zt(e).map(function(t){return" "+t+'="'+e[t]+'"'}).join("")}function Fe(e,t,r){return"<"+e+(r!=null?wa(r):"")+(t!=null?(t.match(xf)?' xml:space="preserve"':"")+">"+t+""}function fl(e,t){try{return e.toISOString().replace(/\.\d*/,"")}catch(r){if(t)throw r}return""}function Tv(e,t){switch(typeof e){case"string":var r=Fe("vt:lpwstr",ht(e));return r=r.replace(/"/g,"_x0022_"),r;case"number":return Fe((e|0)==e?"vt:i4":"vt:r8",ht(String(e)));case"boolean":return Fe("vt:bool",e?"true":"false")}if(e instanceof Date)return Fe("vt:filetime",fl(e));throw new Error("Unable to serialize "+e)}var Ut={CORE_PROPS:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties",CUST_PROPS:"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties",EXT_PROPS:"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties",CT:"http://schemas.openxmlformats.org/package/2006/content-types",RELS:"http://schemas.openxmlformats.org/package/2006/relationships",TCMNT:"http://schemas.microsoft.com/office/spreadsheetml/2018/threadedcomments",dc:"http://purl.org/dc/elements/1.1/",dcterms:"http://purl.org/dc/terms/",dcmitype:"http://purl.org/dc/dcmitype/",r:"http://schemas.openxmlformats.org/officeDocument/2006/relationships",vt:"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes",xsi:"http://www.w3.org/2001/XMLSchema-instance",xsd:"http://www.w3.org/2001/XMLSchema"},Ns=["http://schemas.openxmlformats.org/spreadsheetml/2006/main","http://purl.oclc.org/ooxml/spreadsheetml/main","http://schemas.microsoft.com/office/excel/2006/main","http://schemas.microsoft.com/office/excel/2006/2"],br={o:"urn:schemas-microsoft-com:office:office",x:"urn:schemas-microsoft-com:office:excel",ss:"urn:schemas-microsoft-com:office:spreadsheet",dt:"uuid:C2F41010-65B3-11d1-A29F-00AA00C14882",mv:"http://macVmlSchemaUri",v:"urn:schemas-microsoft-com:vml",html:"http://www.w3.org/TR/REC-html40"};function bv(e,t){for(var r=1-2*(e[t+7]>>>7),n=((e[t+7]&127)<<4)+(e[t+6]>>>4&15),s=e[t+6]&15,i=5;i>=0;--i)s=s*256+e[t+i];return n==2047?s==0?r*(1/0):NaN:(n==0?n=-1022:(n-=1023,s+=Math.pow(2,52)),r*Math.pow(2,n-52)*s)}function Ev(e,t,r){var n=(t<0||1/t==-1/0?1:0)<<7,s=0,i=0,o=n?-t:t;isFinite(o)?o==0?s=i=0:(s=Math.floor(Math.log(o)/Math.LN2),i=o*Math.pow(2,52-s),s<=-1023&&(!isFinite(i)||i>4|n}var bd=function(e){for(var t=[],r=10240,n=0;n0&&Buffer.isBuffer(e[0][0])?Buffer.concat(e[0].map(function(t){return Buffer.isBuffer(t)?t:wn(t)})):bd(e)}:bd,jd=function(e,t,r){for(var n=[],s=t;s0?Na(e,t+4,t+4+r-1):""},_f=gf,vf=function(e,t){var r=Er(e,t);return r>0?Na(e,t+4,t+4+r-1):""},yf=vf,wf=function(e,t){var r=2*Er(e,t);return r>0?Na(e,t+4,t+4+r-1):""},Sf=wf,Tf=function(t,r){var n=Er(t,r);return n>0?zl(t,r+4,r+4+n):""},bf=Tf,Ef=function(e,t){var r=Er(e,t);return r>0?Na(e,t+4,t+4+r):""},jf=Ef,If=function(e,t){return bv(e,t)},Oi=If,$l=function(t){return Array.isArray(t)||typeof Uint8Array<"u"&&t instanceof Uint8Array};ct&&(_f=function(t,r){if(!Buffer.isBuffer(t))return gf(t,r);var n=t.readUInt32LE(r);return n>0?t.toString("utf8",r+4,r+4+n-1):""},yf=function(t,r){if(!Buffer.isBuffer(t))return vf(t,r);var n=t.readUInt32LE(r);return n>0?t.toString("utf8",r+4,r+4+n-1):""},Sf=function(t,r){if(!Buffer.isBuffer(t))return wf(t,r);var n=2*t.readUInt32LE(r);return t.toString("utf16le",r+4,r+4+n-1)},bf=function(t,r){if(!Buffer.isBuffer(t))return Tf(t,r);var n=t.readUInt32LE(r);return t.toString("utf16le",r+4,r+4+n)},jf=function(t,r){if(!Buffer.isBuffer(t))return Ef(t,r);var n=t.readUInt32LE(r);return t.toString("utf8",r+4,r+4+n)},Oi=function(t,r){return Buffer.isBuffer(t)?t.readDoubleLE(r):If(t,r)},$l=function(t){return Buffer.isBuffer(t)||Array.isArray(t)||typeof Uint8Array<"u"&&t instanceof Uint8Array});var gs=function(e,t){return e[t]},qs=function(e,t){return e[t+1]*256+e[t]},jv=function(e,t){var r=e[t+1]*256+e[t];return r<32768?r:(65535-r+1)*-1},Er=function(e,t){return e[t+3]*(1<<24)+(e[t+2]<<16)+(e[t+1]<<8)+e[t]},Hn=function(e,t){return e[t+3]<<24|e[t+2]<<16|e[t+1]<<8|e[t]},Iv=function(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]};function sa(e,t){var r="",n,s,i=[],o,l,c,d;switch(t){case"dbcs":if(d=this.l,ct&&Buffer.isBuffer(this))r=this.slice(this.l,this.l+2*e).toString("utf16le");else for(c=0;c0?Hn:Iv)(this,this.l),this.l+=4,n):(s=Er(this,this.l),this.l+=4,s);case 8:case-8:if(t==="f")return e==8?s=Oi(this,this.l):s=Oi([this[this.l+7],this[this.l+6],this[this.l+5],this[this.l+4],this[this.l+3],this[this.l+2],this[this.l+1],this[this.l+0]],0),this.l+=8,s;e=8;case 16:r=pf(this,this.l,e);break}}return this.l+=e,r}var Cv=function(e,t,r){e[r]=t&255,e[r+1]=t>>>8&255,e[r+2]=t>>>16&255,e[r+3]=t>>>24&255},Av=function(e,t,r){e[r]=t&255,e[r+1]=t>>8&255,e[r+2]=t>>16&255,e[r+3]=t>>24&255},kv=function(e,t,r){e[r]=t&255,e[r+1]=t>>>8&255};function Fv(e,t,r){var n=0,s=0;if(r==="dbcs"){for(s=0;s!=t.length;++s)kv(this,t.charCodeAt(s),this.l+2*s);n=2*t.length}else if(r==="sbcs"){for(t=t.replace(/[^\x00-\x7F]/g,"_"),s=0;s!=t.length;++s)this[this.l+s]=t.charCodeAt(s)&255;n=t.length}else if(r==="hex"){for(;s>8}for(;this.l>>=8,this[this.l+1]=t&255;break;case 3:n=3,this[this.l]=t&255,t>>>=8,this[this.l+1]=t&255,t>>>=8,this[this.l+2]=t&255;break;case 4:n=4,Cv(this,t,this.l);break;case 8:if(n=8,r==="f"){Ev(this,t,this.l);break}case 16:break;case-4:n=4,Av(this,t,this.l);break}return this.l+=n,this}function Cf(e,t){var r=pf(this,this.l,e.length>>1);if(r!==e)throw new Error(t+"Expected "+e+" saw "+r);this.l+=e.length>>1}function Tr(e,t){e.l=t,e.read_shift=sa,e.chk=Cf,e.write_shift=Fv}function an(e,t){e.l+=t}function pe(e){var t=Jn(e);return Tr(t,0),t}function xr(){var e=[],t=ct?256:2048,r=function(d){var u=pe(d);return Tr(u,0),u},n=r(t),s=function(){n&&(n.length>n.l&&(n=n.slice(0,n.l),n.l=n.length),n.length>0&&e.push(n),n=null)},i=function(d){return n&&d=128?1:0)+1,n>=128&&++i,n>=16384&&++i,n>=2097152&&++i;var o=e.next(i);s<=127?o.write_shift(1,s):(o.write_shift(1,(s&127)+128),o.write_shift(1,s>>7));for(var l=0;l!=4;++l)if(n>=128)o.write_shift(1,(n&127)+128),n>>=7;else{o.write_shift(1,n);break}n>0&&$l(r)&&e.push(r)}}function aa(e,t,r){var n=gr(e);if(t.s?(n.cRel&&(n.c+=t.s.c),n.rRel&&(n.r+=t.s.r)):(n.cRel&&(n.c+=t.c),n.rRel&&(n.r+=t.r)),!r||r.biff<12){for(;n.c>=256;)n.c-=256;for(;n.r>=65536;)n.r-=65536}return n}function Ad(e,t,r){var n=gr(e);return n.s=aa(n.s,t.s,r),n.e=aa(n.e,t.s,r),n}function ia(e,t){if(e.cRel&&e.c<0)for(e=gr(e);e.c<0;)e.c+=t>8?16384:256;if(e.rRel&&e.r<0)for(e=gr(e);e.r<0;)e.r+=t>8?1048576:t>5?65536:16384;var r=mt(e);return!e.cRel&&e.cRel!=null&&(r=Dv(r)),!e.rRel&&e.rRel!=null&&(r=Ov(r)),r}function No(e,t){return e.s.r==0&&!e.s.rRel&&e.e.r==(t.biff>=12?1048575:t.biff>=8?65536:16384)&&!e.e.rRel?(e.s.cRel?"":"$")+sr(e.s.c)+":"+(e.e.cRel?"":"$")+sr(e.e.c):e.s.c==0&&!e.s.cRel&&e.e.c==(t.biff>=12?16383:255)&&!e.e.cRel?(e.s.rRel?"":"$")+Jt(e.s.r)+":"+(e.e.rRel?"":"$")+Jt(e.e.r):ia(e.s,t.biff)+":"+ia(e.e,t.biff)}function Wl(e){return parseInt(Rv(e),10)-1}function Jt(e){return""+(e+1)}function Ov(e){return e.replace(/([A-Z]|^)(\d+)$/,"$1$$$2")}function Rv(e){return e.replace(/\$(\d+)$/,"$1")}function Hl(e){for(var t=Pv(e),r=0,n=0;n!==t.length;++n)r=26*r+t.charCodeAt(n)-64;return r-1}function sr(e){if(e<0)throw new Error("invalid column "+e);var t="";for(++e;e;e=Math.floor((e-1)/26))t=String.fromCharCode((e-1)%26+65)+t;return t}function Dv(e){return e.replace(/^([A-Z])/,"$$$1")}function Pv(e){return e.replace(/^\$([A-Z])/,"$1")}function Nv(e){return e.replace(/(\$?[A-Z]*)(\$?\d*)/,"$1,$2").split(",")}function zt(e){for(var t=0,r=0,n=0;n=48&&s<=57?t=10*t+(s-48):s>=65&&s<=90&&(r=26*r+(s-64))}return{c:r-1,r:t-1}}function mt(e){for(var t=e.c+1,r="";t;t=(t-1)/26|0)r=String.fromCharCode((t-1)%26+65)+r;return r+(e.r+1)}function Ir(e){var t=e.indexOf(":");return t==-1?{s:zt(e),e:zt(e)}:{s:zt(e.slice(0,t)),e:zt(e.slice(t+1))}}function Ot(e,t){return typeof t>"u"||typeof t=="number"?Ot(e.s,e.e):(typeof e!="string"&&(e=mt(e)),typeof t!="string"&&(t=mt(t)),e==t?e:e+":"+t)}function vt(e){var t={s:{c:0,r:0},e:{c:0,r:0}},r=0,n=0,s=0,i=e.length;for(r=0;n26);++n)r=26*r+s;for(t.s.c=--r,r=0;n9);++n)r=10*r+s;if(t.s.r=--r,n===i||s!=10)return t.e.c=t.s.c,t.e.r=t.s.r,t;for(++n,r=0;n!=i&&!((s=e.charCodeAt(n)-64)<1||s>26);++n)r=26*r+s;for(t.e.c=--r,r=0;n!=i&&!((s=e.charCodeAt(n)-48)<0||s>9);++n)r=10*r+s;return t.e.r=--r,t}function kd(e,t){var r=e.t=="d"&&t instanceof Date;if(e.z!=null)try{return e.w=Pn(e.z,r?pr(t):t)}catch{}try{return e.w=Pn((e.XF||{}).numFmtId||(r?14:0),r?pr(t):t)}catch{return""+t}}function yn(e,t,r){return e==null||e.t==null||e.t=="z"?"":e.w!==void 0?e.w:(e.t=="d"&&!e.z&&r&&r.dateNF&&(e.z=r.dateNF),e.t=="e"?La[e.v]||e.v:t==null?kd(e,e.v):kd(e,t))}function es(e,t){var r=t&&t.sheet?t.sheet:"Sheet1",n={};return n[r]=e,{SheetNames:[r],Sheets:n}}function Af(e,t,r){var n=r||{},s=e?Array.isArray(e):n.dense,i=e||(s?[]:{}),o=0,l=0;if(i&&n.origin!=null){if(typeof n.origin=="number")o=n.origin;else{var c=typeof n.origin=="string"?zt(n.origin):n.origin;o=c.r,l=c.c}i["!ref"]||(i["!ref"]="A1:A1")}var d={s:{c:1e7,r:1e7},e:{c:0,r:0}};if(i["!ref"]){var u=vt(i["!ref"]);d.s.c=u.s.c,d.s.r=u.s.r,d.e.c=Math.max(d.e.c,u.e.c),d.e.r=Math.max(d.e.r,u.e.r),o==-1&&(d.e.r=o=u.e.r+1)}for(var h=0;h!=t.length;++h)if(t[h]){if(!Array.isArray(t[h]))throw new Error("aoa_to_sheet expects an array of arrays");for(var m=0;m!=t[h].length;++m)if(!(typeof t[h][m]>"u")){var _={v:t[h][m]},x=o+h,f=l+m;if(d.s.r>x&&(d.s.r=x),d.s.c>f&&(d.s.c=f),d.e.r0&&t.write_shift(0,e,"dbcs"),r?t.slice(0,t.l):t}function Mv(e){return{ich:e.read_shift(2),ifnt:e.read_shift(2)}}function Bv(e,t){return t||(t=pe(4)),t.write_shift(2,0),t.write_shift(2,0),t}function Vl(e,t){var r=e.l,n=e.read_shift(1),s=ar(e),i=[],o={t:s,h:s};if(n&1){for(var l=e.read_shift(4),c=0;c!=l;++c)i.push(Mv(e));o.r=i}else o.r=[{ich:0,ifnt:0}];return e.l=r+t,o}function Uv(e,t){var r=!1;return t==null&&(r=!0,t=pe(15+4*e.t.length)),t.write_shift(1,0),$t(e.t,t),r?t.slice(0,t.l):t}var zv=Vl;function $v(e,t){var r=!1;return t==null&&(r=!0,t=pe(23+4*e.t.length)),t.write_shift(1,1),$t(e.t,t),t.write_shift(4,1),Bv({},t),r?t.slice(0,t.l):t}function Br(e){var t=e.read_shift(4),r=e.read_shift(2);return r+=e.read_shift(1)<<16,e.l++,{c:t,iStyleRef:r}}function ts(e,t){return t==null&&(t=pe(8)),t.write_shift(-4,e.c),t.write_shift(3,e.iStyleRef||e.s),t.write_shift(1,0),t}function rs(e){var t=e.read_shift(2);return t+=e.read_shift(1)<<16,e.l++,{c:-1,iStyleRef:t}}function ns(e,t){return t==null&&(t=pe(4)),t.write_shift(3,e.iStyleRef||e.s),t.write_shift(1,0),t}var Wv=ar,kf=$t;function Kl(e){var t=e.read_shift(4);return t===0||t===4294967295?"":e.read_shift(t,"dbcs")}function Ri(e,t){var r=!1;return t==null&&(r=!0,t=pe(127)),t.write_shift(4,e.length>0?e.length:4294967295),e.length>0&&t.write_shift(0,e,"dbcs"),r?t.slice(0,t.l):t}var Hv=ar,hl=Kl,Gl=Ri;function Ff(e){var t=e.slice(e.l,e.l+4),r=t[0]&1,n=t[0]&2;e.l+=4;var s=n===0?Oi([0,0,0,0,t[0]&252,t[1],t[2],t[3]],0):Hn(t,0)>>2;return r?s/100:s}function Of(e,t){t==null&&(t=pe(4));var r=0,n=0,s=e*100;if(e==(e|0)&&e>=-536870912&&e<1<<29?n=1:s==(s|0)&&s>=-536870912&&s<1<<29&&(n=1,r=1),n)t.write_shift(-4,((r?s:e)<<2)+(r+2));else throw new Error("unsupported RkNumber "+e)}function Rf(e){var t={s:{},e:{}};return t.s.r=e.read_shift(4),t.e.r=e.read_shift(4),t.s.c=e.read_shift(4),t.e.c=e.read_shift(4),t}function Vv(e,t){return t||(t=pe(16)),t.write_shift(4,e.s.r),t.write_shift(4,e.e.r),t.write_shift(4,e.s.c),t.write_shift(4,e.e.c),t}var ss=Rf,Ms=Vv;function Bs(e){if(e.length-e.l<8)throw"XLS Xnum Buffer underflow";return e.read_shift(8,"f")}function Zn(e,t){return(t||pe(8)).write_shift(8,e,"f")}function Kv(e){var t={},r=e.read_shift(1),n=r>>>1,s=e.read_shift(1),i=e.read_shift(2,"i"),o=e.read_shift(1),l=e.read_shift(1),c=e.read_shift(1);switch(e.l++,n){case 0:t.auto=1;break;case 1:t.index=s;var d=ty[s];d&&(t.rgb=zd(d));break;case 2:t.rgb=zd([o,l,c]);break;case 3:t.theme=s;break}return i!=0&&(t.tint=i>0?i/32767:i/32768),t}function Di(e,t){if(t||(t=pe(8)),!e||e.auto)return t.write_shift(4,0),t.write_shift(4,0),t;e.index!=null?(t.write_shift(1,2),t.write_shift(1,e.index)):e.theme!=null?(t.write_shift(1,6),t.write_shift(1,e.theme)):(t.write_shift(1,5),t.write_shift(1,0));var r=e.tint||0;if(r>0?r*=32767:r<0&&(r*=32768),t.write_shift(2,r),!e.rgb||e.theme!=null)t.write_shift(2,0),t.write_shift(1,0),t.write_shift(1,0);else{var n=e.rgb||"FFFFFF";typeof n=="number"&&(n=("000000"+n.toString(16)).slice(-6)),t.write_shift(1,parseInt(n.slice(0,2),16)),t.write_shift(1,parseInt(n.slice(2,4),16)),t.write_shift(1,parseInt(n.slice(4,6),16)),t.write_shift(1,255)}return t}function Gv(e){var t=e.read_shift(1);e.l++;var r={fBold:t&1,fItalic:t&2,fUnderline:t&4,fStrikeout:t&8,fOutline:t&16,fShadow:t&32,fCondense:t&64,fExtend:t&128};return r}function Yv(e,t){t||(t=pe(2));var r=(e.italic?2:0)|(e.strike?8:0)|(e.outline?16:0)|(e.shadow?32:0)|(e.condense?64:0)|(e.extend?128:0);return t.write_shift(1,r),t.write_shift(1,0),t}var Df=2,Sr=3,ni=11,Pi=19,si=64,qv=65,Xv=71,Jv=4108,Zv=4126,Kt=80,Fd={1:{n:"CodePage",t:Df},2:{n:"Category",t:Kt},3:{n:"PresentationFormat",t:Kt},4:{n:"ByteCount",t:Sr},5:{n:"LineCount",t:Sr},6:{n:"ParagraphCount",t:Sr},7:{n:"SlideCount",t:Sr},8:{n:"NoteCount",t:Sr},9:{n:"HiddenCount",t:Sr},10:{n:"MultimediaClipCount",t:Sr},11:{n:"ScaleCrop",t:ni},12:{n:"HeadingPairs",t:Jv},13:{n:"TitlesOfParts",t:Zv},14:{n:"Manager",t:Kt},15:{n:"Company",t:Kt},16:{n:"LinksUpToDate",t:ni},17:{n:"CharacterCount",t:Sr},19:{n:"SharedDoc",t:ni},22:{n:"HyperlinksChanged",t:ni},23:{n:"AppVersion",t:Sr,p:"version"},24:{n:"DigSig",t:qv},26:{n:"ContentType",t:Kt},27:{n:"ContentStatus",t:Kt},28:{n:"Language",t:Kt},29:{n:"Version",t:Kt},255:{},2147483648:{n:"Locale",t:Pi},2147483651:{n:"Behavior",t:Pi},1919054434:{}},Od={1:{n:"CodePage",t:Df},2:{n:"Title",t:Kt},3:{n:"Subject",t:Kt},4:{n:"Author",t:Kt},5:{n:"Keywords",t:Kt},6:{n:"Comments",t:Kt},7:{n:"Template",t:Kt},8:{n:"LastAuthor",t:Kt},9:{n:"RevNumber",t:Kt},10:{n:"EditTime",t:si},11:{n:"LastPrinted",t:si},12:{n:"CreatedDate",t:si},13:{n:"ModifiedDate",t:si},14:{n:"PageCount",t:Sr},15:{n:"WordCount",t:Sr},16:{n:"CharCount",t:Sr},17:{n:"Thumbnail",t:Xv},18:{n:"Application",t:Kt},19:{n:"DocSecurity",t:Sr},255:{},2147483648:{n:"Locale",t:Pi},2147483651:{n:"Behavior",t:Pi},1919054434:{}};function Qv(e){return e.map(function(t){return[t>>16&255,t>>8&255,t&255]})}var ey=Qv([0,16777215,16711680,65280,255,16776960,16711935,65535,0,16777215,16711680,65280,255,16776960,16711935,65535,8388608,32768,128,8421376,8388736,32896,12632256,8421504,10066431,10040166,16777164,13434879,6684774,16744576,26316,13421823,128,16711935,16776960,65535,8388736,8388608,32896,255,52479,13434879,13434828,16777113,10079487,16751052,13408767,16764057,3368703,3394764,10079232,16763904,16750848,16737792,6710937,9868950,13158,3381606,13056,3355392,10040064,10040166,3355545,3355443,16777215,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]),ty=gr(ey),La={0:"#NULL!",7:"#DIV/0!",15:"#VALUE!",23:"#REF!",29:"#NAME?",36:"#NUM!",42:"#N/A",43:"#GETTING_DATA",255:"#WTF?"},ry={"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml":"workbooks","application/vnd.ms-excel.sheet.macroEnabled.main+xml":"workbooks","application/vnd.ms-excel.sheet.binary.macroEnabled.main":"workbooks","application/vnd.ms-excel.addin.macroEnabled.main+xml":"workbooks","application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml":"workbooks","application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml":"sheets","application/vnd.ms-excel.worksheet":"sheets","application/vnd.ms-excel.binIndexWs":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml":"charts","application/vnd.ms-excel.chartsheet":"charts","application/vnd.ms-excel.macrosheet+xml":"macros","application/vnd.ms-excel.macrosheet":"macros","application/vnd.ms-excel.intlmacrosheet":"TODO","application/vnd.ms-excel.binIndexMs":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml":"dialogs","application/vnd.ms-excel.dialogsheet":"dialogs","application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml":"strs","application/vnd.ms-excel.sharedStrings":"strs","application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml":"styles","application/vnd.ms-excel.styles":"styles","application/vnd.openxmlformats-package.core-properties+xml":"coreprops","application/vnd.openxmlformats-officedocument.custom-properties+xml":"custprops","application/vnd.openxmlformats-officedocument.extended-properties+xml":"extprops","application/vnd.openxmlformats-officedocument.customXmlProperties+xml":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml":"comments","application/vnd.ms-excel.comments":"comments","application/vnd.ms-excel.threadedcomments+xml":"threadedcomments","application/vnd.ms-excel.person+xml":"people","application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml":"metadata","application/vnd.ms-excel.sheetMetadata":"metadata","application/vnd.ms-excel.pivotTable":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml":"TODO","application/vnd.openxmlformats-officedocument.drawingml.chart+xml":"TODO","application/vnd.ms-office.chartcolorstyle+xml":"TODO","application/vnd.ms-office.chartstyle+xml":"TODO","application/vnd.ms-office.chartex+xml":"TODO","application/vnd.ms-excel.calcChain":"calcchains","application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml":"calcchains","application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings":"TODO","application/vnd.ms-office.activeX":"TODO","application/vnd.ms-office.activeX+xml":"TODO","application/vnd.ms-excel.attachedToolbars":"TODO","application/vnd.ms-excel.connections":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml":"TODO","application/vnd.ms-excel.externalLink":"links","application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml":"links","application/vnd.ms-excel.pivotCacheDefinition":"TODO","application/vnd.ms-excel.pivotCacheRecords":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml":"TODO","application/vnd.ms-excel.queryTable":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml":"TODO","application/vnd.ms-excel.userNames":"TODO","application/vnd.ms-excel.revisionHeaders":"TODO","application/vnd.ms-excel.revisionLog":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml":"TODO","application/vnd.ms-excel.tableSingleCells":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml":"TODO","application/vnd.ms-excel.slicer":"TODO","application/vnd.ms-excel.slicerCache":"TODO","application/vnd.ms-excel.slicer+xml":"TODO","application/vnd.ms-excel.slicerCache+xml":"TODO","application/vnd.ms-excel.wsSortMap":"TODO","application/vnd.ms-excel.table":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml":"TODO","application/vnd.openxmlformats-officedocument.theme+xml":"themes","application/vnd.openxmlformats-officedocument.themeOverride+xml":"TODO","application/vnd.ms-excel.Timeline+xml":"TODO","application/vnd.ms-excel.TimelineCache+xml":"TODO","application/vnd.ms-office.vbaProject":"vba","application/vnd.ms-office.vbaProjectSignature":"TODO","application/vnd.ms-office.volatileDependencies":"TODO","application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml":"TODO","application/vnd.ms-excel.controlproperties+xml":"TODO","application/vnd.openxmlformats-officedocument.model+data":"TODO","application/vnd.ms-excel.Survey+xml":"TODO","application/vnd.openxmlformats-officedocument.drawing+xml":"drawings","application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml":"TODO","application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml":"TODO","application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml":"TODO","application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml":"TODO","application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml":"TODO","application/vnd.openxmlformats-officedocument.vmlDrawing":"TODO","application/vnd.openxmlformats-package.relationships+xml":"rels","application/vnd.openxmlformats-officedocument.oleObject":"TODO","image/png":"TODO",sheet:"js"},ai={workbooks:{xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",xlsm:"application/vnd.ms-excel.sheet.macroEnabled.main+xml",xlsb:"application/vnd.ms-excel.sheet.binary.macroEnabled.main",xlam:"application/vnd.ms-excel.addin.macroEnabled.main+xml",xltx:"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"},strs:{xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",xlsb:"application/vnd.ms-excel.sharedStrings"},comments:{xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml",xlsb:"application/vnd.ms-excel.comments"},sheets:{xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",xlsb:"application/vnd.ms-excel.worksheet"},charts:{xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml",xlsb:"application/vnd.ms-excel.chartsheet"},dialogs:{xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml",xlsb:"application/vnd.ms-excel.dialogsheet"},macros:{xlsx:"application/vnd.ms-excel.macrosheet+xml",xlsb:"application/vnd.ms-excel.macrosheet"},metadata:{xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",xlsb:"application/vnd.ms-excel.sheetMetadata"},styles:{xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",xlsb:"application/vnd.ms-excel.styles"}};function Pf(){return{workbooks:[],sheets:[],charts:[],dialogs:[],macros:[],rels:[],strs:[],comments:[],threadedcomments:[],links:[],coreprops:[],extprops:[],custprops:[],themes:[],styles:[],calcchains:[],vba:[],drawings:[],metadata:[],people:[],TODO:[],xmlns:""}}function Nf(e,t){var r=fv(ry),n=[],s;n[n.length]=Rt,n[n.length]=Fe("Types",null,{xmlns:Ut.CT,"xmlns:xsd":Ut.xsd,"xmlns:xsi":Ut.xsi}),n=n.concat([["xml","application/xml"],["bin","application/vnd.ms-excel.sheet.binary.macroEnabled.main"],["vml","application/vnd.openxmlformats-officedocument.vmlDrawing"],["data","application/vnd.openxmlformats-officedocument.model+data"],["bmp","image/bmp"],["png","image/png"],["gif","image/gif"],["emf","image/x-emf"],["wmf","image/x-wmf"],["jpg","image/jpeg"],["jpeg","image/jpeg"],["tif","image/tiff"],["tiff","image/tiff"],["pdf","application/pdf"],["rels","application/vnd.openxmlformats-package.relationships+xml"]].map(function(c){return Fe("Default",null,{Extension:c[0],ContentType:c[1]})}));var i=function(c){e[c]&&e[c].length>0&&(s=e[c][0],n[n.length]=Fe("Override",null,{PartName:(s[0]=="/"?"":"/")+s,ContentType:ai[c][t.bookType]||ai[c].xlsx}))},o=function(c){(e[c]||[]).forEach(function(d){n[n.length]=Fe("Override",null,{PartName:(d[0]=="/"?"":"/")+d,ContentType:ai[c][t.bookType]||ai[c].xlsx})})},l=function(c){(e[c]||[]).forEach(function(d){n[n.length]=Fe("Override",null,{PartName:(d[0]=="/"?"":"/")+d,ContentType:r[c][0]})})};return i("workbooks"),o("sheets"),o("charts"),l("themes"),["strs","styles"].forEach(i),["coreprops","extprops","custprops"].forEach(l),l("vba"),l("comments"),l("threadedcomments"),l("drawings"),o("metadata"),l("people"),n.length>2&&(n[n.length]="",n[1]=n[1].replace("/>",">")),n.join("")}var it={WB:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",HLINK:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",VML:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing",XPATH:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath",XMISS:"http://schemas.microsoft.com/office/2006/relationships/xlExternalLinkPath/xlPathMissing",CMNT:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",CORE_PROPS:"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",EXT_PROPS:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",CUST_PROPS:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties",SST:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",STY:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",THEME:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",WS:["http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet","http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet"],DRAW:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",XLMETA:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",TCMNT:"http://schemas.microsoft.com/office/2017/10/relationships/threadedComment",PEOPLE:"http://schemas.microsoft.com/office/2017/10/relationships/person",VBA:"http://schemas.microsoft.com/office/2006/relationships/vbaProject"};function Lf(e){var t=e.lastIndexOf("/");return e.slice(0,t+1)+"_rels/"+e.slice(t+1)+".rels"}function Ss(e){var t=[Rt,Fe("Relationships",null,{xmlns:Ut.RELS})];return Zt(e["!id"]).forEach(function(r){t[t.length]=Fe("Relationship",null,e["!id"][r])}),t.length>2&&(t[t.length]="",t[1]=t[1].replace("/>",">")),t.join("")}function ft(e,t,r,n,s,i){if(s||(s={}),e["!id"]||(e["!id"]={}),e["!idx"]||(e["!idx"]=1),t<0)for(t=e["!idx"];e["!id"]["rId"+t];++t);if(e["!idx"]=t+1,s.Id="rId"+t,s.Type=n,s.Target=r,[it.HLINK,it.XPATH,it.XMISS].indexOf(s.Type)>-1&&(s.TargetMode="External"),e["!id"][s.Id])throw new Error("Cannot rewrite rId "+t);return e["!id"][s.Id]=s,e[("/"+s.Target).replace("//","/")]=s,t}function ny(e){var t=[Rt];t.push(` +`),t.push(` +`);for(var r=0;r +`);return t.push(""),t.join("")}function Rd(e,t,r){return[' +`,' +`,` +`].join("")}function sy(e,t){return[' +`,' +`,` +`].join("")}function ay(e){var t=[Rt];t.push(` +`);for(var r=0;r!=e.length;++r)t.push(Rd(e[r][0],e[r][1])),t.push(sy("",e[r][0]));return t.push(Rd("","Document","pkg")),t.push(""),t.join("")}function Mf(){return'SheetJS '+ji.version+""}var Yn=[["cp:category","Category"],["cp:contentStatus","ContentStatus"],["cp:keywords","Keywords"],["cp:lastModifiedBy","LastAuthor"],["cp:lastPrinted","LastPrinted"],["cp:revision","RevNumber"],["cp:version","Version"],["dc:creator","Author"],["dc:description","Comments"],["dc:identifier","Identifier"],["dc:language","Language"],["dc:subject","Subject"],["dc:title","Title"],["dcterms:created","CreatedDate","date"],["dcterms:modified","ModifiedDate","date"]];function Lo(e,t,r,n,s){s[e]!=null||t==null||t===""||(s[e]=t,t=ht(t),n[n.length]=r?Fe(e,t,r):qt(e,t))}function Bf(e,t){var r=t||{},n=[Rt,Fe("cp:coreProperties",null,{"xmlns:cp":Ut.CORE_PROPS,"xmlns:dc":Ut.dc,"xmlns:dcterms":Ut.dcterms,"xmlns:dcmitype":Ut.dcmitype,"xmlns:xsi":Ut.xsi})],s={};if(!e&&!r.Props)return n.join("");e&&(e.CreatedDate!=null&&Lo("dcterms:created",typeof e.CreatedDate=="string"?e.CreatedDate:fl(e.CreatedDate,r.WTF),{"xsi:type":"dcterms:W3CDTF"},n,s),e.ModifiedDate!=null&&Lo("dcterms:modified",typeof e.ModifiedDate=="string"?e.ModifiedDate:fl(e.ModifiedDate,r.WTF),{"xsi:type":"dcterms:W3CDTF"},n,s));for(var i=0;i!=Yn.length;++i){var o=Yn[i],l=r.Props&&r.Props[o[1]]!=null?r.Props[o[1]]:e?e[o[1]]:null;l===!0?l="1":l===!1?l="0":typeof l=="number"&&(l=String(l)),l!=null&&Lo(o[0],l,null,n,s)}return n.length>2&&(n[n.length]="",n[1]=n[1].replace("/>",">")),n.join("")}var Ts=[["Application","Application","string"],["AppVersion","AppVersion","string"],["Company","Company","string"],["DocSecurity","DocSecurity","string"],["Manager","Manager","string"],["HyperlinksChanged","HyperlinksChanged","bool"],["SharedDoc","SharedDoc","bool"],["LinksUpToDate","LinksUpToDate","bool"],["ScaleCrop","ScaleCrop","bool"],["HeadingPairs","HeadingPairs","raw"],["TitlesOfParts","TitlesOfParts","raw"]],Uf=["Worksheets","SheetNames","NamedRanges","DefinedNames","Chartsheets","ChartNames"];function zf(e){var t=[],r=Fe;return e||(e={}),e.Application="SheetJS",t[t.length]=Rt,t[t.length]=Fe("Properties",null,{xmlns:Ut.EXT_PROPS,"xmlns:vt":Ut.vt}),Ts.forEach(function(n){if(e[n[1]]!==void 0){var s;switch(n[2]){case"string":s=ht(String(e[n[1]]));break;case"bool":s=e[n[1]]?"true":"false";break}s!==void 0&&(t[t.length]=r(n[0],s))}}),t[t.length]=r("HeadingPairs",r("vt:vector",r("vt:variant","Worksheets")+r("vt:variant",r("vt:i4",String(e.Worksheets))),{size:2,baseType:"variant"})),t[t.length]=r("TitlesOfParts",r("vt:vector",e.SheetNames.map(function(n){return""+ht(n)+""}).join(""),{size:e.Worksheets,baseType:"lpstr"})),t.length>2&&(t[t.length]="",t[1]=t[1].replace("/>",">")),t.join("")}function $f(e){var t=[Rt,Fe("Properties",null,{xmlns:Ut.CUST_PROPS,"xmlns:vt":Ut.vt})];if(!e)return t.join("");var r=1;return Zt(e).forEach(function(s){++r,t[t.length]=Fe("property",Tv(e[s]),{fmtid:"{D5CDD505-2E9C-101B-9397-08002B2CF9AE}",pid:r,name:ht(s)})}),t.length>2&&(t[t.length]="",t[1]=t[1].replace("/>",">")),t.join("")}var Dd={Title:"Title",Subject:"Subject",Author:"Author",Keywords:"Keywords",Comments:"Description",LastAuthor:"LastAuthor",RevNumber:"Revision",Application:"AppName",LastPrinted:"LastPrinted",CreatedDate:"Created",ModifiedDate:"LastSaved",Category:"Category",Manager:"Manager",Company:"Company",AppVersion:"Version",ContentStatus:"ContentStatus",Identifier:"Identifier",Language:"Language"};function iy(e,t){var r=[];return Zt(Dd).map(function(n){for(var s=0;s'+s.join("")+""}function ly(e){var t=typeof e=="string"?new Date(Date.parse(e)):e,r=t.getTime()/1e3+11644473600,n=r%Math.pow(2,32),s=(r-n)/Math.pow(2,32);n*=1e7,s*=1e7;var i=n/Math.pow(2,32)|0;i>0&&(n=n%Math.pow(2,32),s+=i);var o=pe(8);return o.write_shift(4,n),o.write_shift(4,s),o}function Pd(e,t){var r=pe(4),n=pe(4);switch(r.write_shift(4,e==80?31:e),e){case 3:n.write_shift(-4,t);break;case 5:n=pe(8),n.write_shift(8,t,"f");break;case 11:n.write_shift(4,t?1:0);break;case 64:n=ly(t);break;case 31:case 80:for(n=pe(4+2*(t.length+1)+(t.length%2?0:2)),n.write_shift(4,t.length+1),n.write_shift(0,t,"dbcs");n.l!=n.length;)n.write_shift(1,0);break;default:throw new Error("TypedPropertyValue unrecognized type "+e+" "+t)}return Yt([r,n])}var Wf=["CodePage","Thumbnail","_PID_LINKBASE","_PID_HLINKS","SystemIdentifier","FMTID"];function cy(e){switch(typeof e){case"boolean":return 11;case"number":return(e|0)==e?3:5;case"string":return 31;case"object":if(e instanceof Date)return 64;break}return-1}function Nd(e,t,r){var n=pe(8),s=[],i=[],o=8,l=0,c=pe(8),d=pe(8);if(c.write_shift(4,2),c.write_shift(4,1200),d.write_shift(4,1),i.push(c),s.push(d),o+=8+c.length,!t){d=pe(8),d.write_shift(4,0),s.unshift(d);var u=[pe(4)];for(u[0].write_shift(4,e.length),l=0;l-1||Uf.indexOf(e[l][0])>-1)&&e[l][1]!=null){var m=e[l][1],_=0;if(t){_=+t[e[l][0]];var x=r[_];if(x.p=="version"&&typeof m=="string"){var f=m.split(".");m=(+f[0]<<16)+(+f[1]||0)}c=Pd(x.t,m)}else{var p=cy(m);p==-1&&(p=31,m=String(m)),c=Pd(p,m)}i.push(c),d=pe(8),d.write_shift(4,t?_:2+l),s.push(d),o+=8+c.length}var I=8*(i.length+1);for(l=0;l=12?2:1),s="sbcs-cont";if(r&&r.biff>=8,!r||r.biff==8){var i=e.read_shift(1);i&&(s="dbcs-cont")}else r.biff==12&&(s="wstr");r.biff>=2&&r.biff<=5&&(s="cpstr");var o=n?e.read_shift(n,s):"";return o}function fy(e){var t=e.t||"",r=pe(3);r.write_shift(2,t.length),r.write_shift(1,1);var n=pe(2*t.length);n.write_shift(2*t.length,t,"utf16le");var s=[r,n];return Yt(s)}function hy(e,t,r){var n;if(r){if(r.biff>=2&&r.biff<=5)return e.read_shift(t,"cpstr");if(r.biff>=12)return e.read_shift(t,"dbcs-cont")}var s=e.read_shift(1);return s===0?n=e.read_shift(t,"sbcs-cont"):n=e.read_shift(t,"dbcs-cont"),n}function my(e,t,r){var n=e.read_shift(r&&r.biff==2?1:2);return n===0?(e.l++,""):hy(e,n,r)}function xy(e,t,r){if(r.biff>5)return my(e,t,r);var n=e.read_shift(1);return n===0?(e.l++,""):e.read_shift(n,r.biff<=4||!e.lens?"cpstr":"sbcs-cont")}function Gf(e,t,r){return r||(r=pe(3+2*e.length)),r.write_shift(2,e.length),r.write_shift(1,1),r.write_shift(31,e,"utf16le"),r}function Md(e,t){t||(t=pe(6+e.length*2)),t.write_shift(4,1+e.length);for(var r=0;r-1?31:23;switch(n.charAt(0)){case"#":i=28;break;case".":i&=-3;break}t.write_shift(4,2),t.write_shift(4,i);var o=[8,6815827,6619237,4849780,83];for(r=0;r-1?n.slice(0,s):n;for(t.write_shift(4,2*(l.length+1)),r=0;r-1?n.slice(s+1):"",t)}else{for(o="03 03 00 00 00 00 00 00 c0 00 00 00 00 00 00 46".split(" "),r=0;r8?4:2,s=e.read_shift(n),i=e.read_shift(n,"i"),o=e.read_shift(n,"i");return[s,i,o]}function _y(e){var t=e.read_shift(2),r=e.read_shift(2),n=e.read_shift(2),s=e.read_shift(2);return{s:{c:n,r:t},e:{c:s,r}}}function Yf(e,t){return t||(t=pe(8)),t.write_shift(2,e.s.r),t.write_shift(2,e.e.r),t.write_shift(2,e.s.c),t.write_shift(2,e.e.c),t}function Yl(e,t,r){var n=1536,s=16;switch(r.bookType){case"biff8":break;case"biff5":n=1280,s=8;break;case"biff4":n=4,s=6;break;case"biff3":n=3,s=6;break;case"biff2":n=2,s=4;break;case"xla":break;default:throw new Error("unsupported BIFF version")}var i=pe(s);return i.write_shift(2,n),i.write_shift(2,t),s>4&&i.write_shift(2,29282),s>6&&i.write_shift(2,1997),s>8&&(i.write_shift(2,49161),i.write_shift(2,1),i.write_shift(2,1798),i.write_shift(2,0)),i}function vy(e,t){var r=!t||t.biff==8,n=pe(r?112:54);for(n.write_shift(t.biff==8?2:1,7),r&&n.write_shift(1,0),n.write_shift(4,859007059),n.write_shift(4,5458548|(r?0:536870912));n.l=8?2:1,n=pe(8+r*e.name.length);n.write_shift(4,e.pos),n.write_shift(1,e.hs||0),n.write_shift(1,e.dt),n.write_shift(1,e.name.length),t.biff>=8&&n.write_shift(1,1),n.write_shift(r*e.name.length,e.name,t.biff<8?"sbcs":"utf16le");var s=n.slice(0,n.l);return s.l=n.l,s}function wy(e,t){var r=pe(8);r.write_shift(4,e.Count),r.write_shift(4,e.Unique);for(var n=[],s=0;sn.l?n.slice(0,n.l):n;return i.l==null&&(i.l=i.length),i}function Cy(e,t){var r=t.biff==8||!t.biff?4:2,n=pe(2*r+6);return n.write_shift(r,e.s.r),n.write_shift(r,e.e.r+1),n.write_shift(2,e.s.c),n.write_shift(2,e.e.c+1),n.write_shift(2,0),n}function Bd(e,t,r,n){var s=r&&r.biff==5;n||(n=pe(s?16:20)),n.write_shift(2,0),e.style?(n.write_shift(2,e.numFmtId||0),n.write_shift(2,65524)):(n.write_shift(2,e.numFmtId||0),n.write_shift(2,t<<4));var i=0;return e.numFmtId>0&&s&&(i|=1024),n.write_shift(4,i),n.write_shift(4,0),s||n.write_shift(4,0),n.write_shift(2,0),n}function Ay(e){var t=pe(8);return t.write_shift(4,0),t.write_shift(2,0),t.write_shift(2,0),t}function ky(e,t,r,n,s,i){var o=pe(8);return Qn(e,t,n,o),Vf(r,i,o),o}function Fy(e,t,r,n){var s=pe(14);return Qn(e,t,n,s),Zn(r,s),s}function Oy(e,t,r){if(r.biff<8)return Ry(e,t,r);for(var n=[],s=e.l+t,i=e.read_shift(r.biff>8?4:2);i--!==0;)n.push(gy(e,r.biff>8?12:6,r));if(e.l!=s)throw new Error("Bad ExternSheet: "+e.l+" != "+s);return n}function Ry(e,t,r){e[e.l+1]==3&&e[e.l]++;var n=Kf(e,t,r);return n.charCodeAt(0)==3?n.slice(1):n}function Dy(e){var t=pe(2+e.length*8);t.write_shift(2,e.length);for(var r=0;r=12?4:2,s=e.read_shift(n),i=e.read_shift(n),o=e.read_shift(n),l=e.read_shift(n),c=e.read_shift(2);n==2&&(e.l+=2);var d={s,e:i,w:o,ixfe:l,flags:c};return(r.biff>=5||!r.biff)&&(d.level=c>>8&7),d}function By(e,t){var r=pe(12);r.write_shift(2,t),r.write_shift(2,t),r.write_shift(2,e.width*256),r.write_shift(2,0);var n=0;return e.hidden&&(n|=1),r.write_shift(1,n),n=e.level||0,r.write_shift(1,n),r.write_shift(2,0),r}function Uy(e){for(var t=pe(2*e),r=0;r1048576&&(f=1e6),h!=2&&(p=u.read_shift(2));var I=u.read_shift(2),S=c.codepage||1252;h!=2&&(u.l+=16,u.read_shift(1),u[u.l]!==0&&(S=e[u[u.l]]),u.l+=1,u.l+=2),x&&(u.l+=36);for(var w=[],v={},y=Math.min(u.length,h==2?521:p-10-(_?264:0)),R=x?32:11;u.l0;){if(u[u.l]===42){u.l+=I;continue}for(++u.l,d[++j]=[],T=0,T=0;T!=w.length;++T){var C=u.slice(u.l,u.l+w[T].len);u.l+=w[T].len,Tr(C,0);var F=Ii.utils.decode(S,C);switch(w[T].type){case"C":F.trim().length&&(d[j][T]=F.replace(/\s+$/,""));break;case"D":F.length===8?d[j][T]=new Date(+F.slice(0,4),+F.slice(4,6)-1,+F.slice(6,8)):d[j][T]=F;break;case"F":d[j][T]=parseFloat(F.trim());break;case"+":case"I":d[j][T]=x?C.read_shift(-4,"i")^2147483648:C.read_shift(4,"i");break;case"L":switch(F.trim().toUpperCase()){case"Y":case"T":d[j][T]=!0;break;case"N":case"F":d[j][T]=!1;break;case"":case"?":break;default:throw new Error("DBF Unrecognized L:|"+F+"|")}break;case"M":if(!m)throw new Error("DBF Unexpected MEMO for type "+h.toString(16));d[j][T]="##MEMO##"+(x?parseInt(F.trim(),10):C.read_shift(4));break;case"N":F=F.replace(/\u0000/g,"").trim(),F&&F!="."&&(d[j][T]=+F||0);break;case"@":d[j][T]=new Date(C.read_shift(-8,"f")-621356832e5);break;case"T":d[j][T]=new Date((C.read_shift(4)-2440588)*864e5+C.read_shift(4));break;case"Y":d[j][T]=C.read_shift(4,"i")/1e4+C.read_shift(4,"i")/1e4*Math.pow(2,32);break;case"O":d[j][T]=-C.read_shift(-8,"f");break;case"B":if(_&&w[T].len==8){d[j][T]=C.read_shift(8,"f");break}case"G":case"P":C.l+=w[T].len;break;case"0":if(w[T].name==="_NullFlags")break;default:throw new Error("DBF Unsupported data type "+w[T].type)}}}if(h!=2&&u.l=0&&ga(+d.codepage),d.type=="string")throw new Error("Cannot write DBF to JS string");var u=xr(),h=Ui(l,{header:1,raw:!0,cellDates:!0}),m=h[0],_=h.slice(1),x=l["!cols"]||[],f=0,p=0,I=0,S=1;for(f=0;f250&&(C=250),T=((x[f]||{}).DBF||{}).type,T=="C"&&x[f].DBF.len>C&&(C=x[f].DBF.len),j=="B"&&T=="N"&&(j="N",R[f]=x[f].DBF.dec,C=x[f].DBF.len),y[f]=j=="C"||T=="N"?C:i[j]||0,S+=y[f],v[f]=j}var P=u.next(32);for(P.write_shift(4,318902576),P.write_shift(4,_.length),P.write_shift(2,296+32*I),P.write_shift(2,S),f=0;f<4;++f)P.write_shift(4,0);for(P.write_shift(4,0|(+t[q0]||3)<<8),f=0,p=0;f":190,"?":191,"{":223},t=new RegExp("\x1BN("+Zt(e).join("|").replace(/\|\|\|/,"|\\||").replace(/([?()+])/g,"\\$1")+"|\\|)","gm"),r=function(m,_){var x=e[_];return typeof x=="number"?cd(x):x},n=function(m,_,x){var f=_.charCodeAt(0)-32<<4|x.charCodeAt(0)-48;return f==59?m:cd(f)};e["|"]=254;function s(m,_){switch(_.type){case"base64":return i(vn(m),_);case"binary":return i(m,_);case"buffer":return i(ct&&Buffer.isBuffer(m)?m.toString("binary"):Da(m),_);case"array":return i(fo(m),_)}throw new Error("Unrecognized type "+_.type)}function i(m,_){var x=m.split(/[\n\r]+/),f=-1,p=-1,I=0,S=0,w=[],v=[],y=null,R={},j=[],T=[],C=[],F=0,P;for(+_.codepage>=0&&ga(+_.codepage);I!==x.length;++I){F=0;var V=x[I].trim().replace(/\x1B([\x20-\x2F])([\x30-\x3F])/g,n).replace(t,r),X=V.replace(/;;/g,"\0").split(";").map(function(D){return D.replace(/\u0000/g,";")}),_e=X[0],B;if(V.length>0)switch(_e){case"ID":break;case"E":break;case"B":break;case"O":break;case"W":break;case"P":X[1].charAt(0)=="P"&&v.push(V.slice(3).replace(/;;/g,";"));break;case"C":var te=!1,J=!1,G=!1,se=!1,Q=-1,de=-1;for(S=1;S-1&&w[Q][de];if(!W||!W[1])throw new Error("SYLK shared formula cannot find base");w[f][p][1]=Ww(W[1],{r:f-Q,c:p-de})}break;case"F":var z=0;for(S=1;S0?(j[f].hpt=F,j[f].hpx=Qf(F)):F===0&&(j[f].hidden=!0);break;default:if(_&&_.WTF)throw new Error("SYLK bad record "+V)}z<1&&(y=null);break;default:if(_&&_.WTF)throw new Error("SYLK bad record "+V)}}return j.length>0&&(R["!rows"]=j),T.length>0&&(R["!cols"]=T),_&&_.sheetRows&&(w=w.slice(0,_.sheetRows)),[w,R]}function o(m,_){var x=s(m,_),f=x[0],p=x[1],I=Ls(f,_);return Zt(p).forEach(function(S){I[S]=p[S]}),I}function l(m,_){return es(o(m,_),_)}function c(m,_,x,f){var p="C;Y"+(x+1)+";X"+(f+1)+";K";switch(m.t){case"n":p+=m.v||0,m.f&&!m.F&&(p+=";E"+Jl(m.f,{r:x,c:f}));break;case"b":p+=m.v?"TRUE":"FALSE";break;case"e":p+=m.w||m.v;break;case"d":p+='"'+(m.w||m.v)+'"';break;case"s":p+='"'+m.v.replace(/"/g,"").replace(/;/g,";;")+'"';break}return p}function d(m,_){_.forEach(function(x,f){var p="F;W"+(f+1)+" "+(f+1)+" ";x.hidden?p+="0":(typeof x.width=="number"&&!x.wpx&&(x.wpx=Ni(x.width)),typeof x.wpx=="number"&&!x.wch&&(x.wch=Li(x.wpx)),typeof x.wch=="number"&&(p+=Math.round(x.wch))),p.charAt(p.length-1)!=" "&&m.push(p)})}function u(m,_){_.forEach(function(x,f){var p="F;";x.hidden?p+="M0;":x.hpt?p+="M"+20*x.hpt+";":x.hpx&&(p+="M"+20*Mi(x.hpx)+";"),p.length>2&&m.push(p+"R"+(f+1))})}function h(m,_){var x=["ID;PWXL;N;E"],f=[],p=vt(m["!ref"]),I,S=Array.isArray(m),w=`\r +`;x.push("P;PGeneral"),x.push("F;P0;DG0G8;M255"),m["!cols"]&&d(x,m["!cols"]),m["!rows"]&&u(x,m["!rows"]),x.push("B;Y"+(p.e.r-p.s.r+1)+";X"+(p.e.c-p.s.c+1)+";D"+[p.s.c,p.s.r,p.e.c,p.e.r].join(" "));for(var v=p.s.r;v<=p.e.r;++v)for(var y=p.s.c;y<=p.e.c;++y){var R=mt({r:v,c:y});I=S?(m[v]||[])[y]:m[R],!(!I||I.v==null&&(!I.f||I.F))&&f.push(c(I,m,v,y))}return x.join(w)+w+f.join(w)+w+"E"+w}return{to_workbook:l,to_sheet:o,from_sheet:h}}(),Vy=function(){function e(i,o){switch(o.type){case"base64":return t(vn(i),o);case"binary":return t(i,o);case"buffer":return t(ct&&Buffer.isBuffer(i)?i.toString("binary"):Da(i),o);case"array":return t(fo(i),o)}throw new Error("Unrecognized type "+o.type)}function t(i,o){for(var l=i.split(` +`),c=-1,d=-1,u=0,h=[];u!==l.length;++u){if(l[u].trim()==="BOT"){h[++c]=[],d=0;continue}if(!(c<0)){var m=l[u].trim().split(","),_=m[0],x=m[1];++u;for(var f=l[u]||"";(f.match(/["]/g)||[]).length&1&&u=0&&x[f].length===0;)--f;for(var p=10,I=0,S=0;S<=f;++S)I=x[S].indexOf(" "),I==-1?I=x[S].length:I++,p=Math.max(p,I);for(S=0;S<=f;++S){_[S]=[];var w=0;for(e(x[S].slice(0,p).trim(),_,S,w,m),w=1;w<=(x[S].length-p)/10+1;++w)e(x[S].slice(p+(w-1)*10,p+w*10).trim(),_,S,w,m)}return m.sheetRows&&(_=_.slice(0,m.sheetRows)),_}var r={44:",",9:" ",59:";",124:"|"},n={44:3,9:2,59:1,124:0};function s(u){for(var h={},m=!1,_=0,x=0;_0&&F(),x["!ref"]=Ot(f),x}function o(u,h){return!(h&&h.PRN)||h.FS||u.slice(0,4)=="sep="||u.indexOf(" ")>=0||u.indexOf(",")>=0||u.indexOf(";")>=0?i(u,h):Ls(t(u,h),h)}function l(u,h){var m="",_=h.type=="string"?[0,0,0,0]:sE(u,h);switch(h.type){case"base64":m=vn(u);break;case"binary":m=u;break;case"buffer":h.codepage==65001?m=u.toString("utf8"):h.codepage&&typeof Ii<"u"||(m=ct&&Buffer.isBuffer(u)?u.toString("binary"):Da(u));break;case"array":m=fo(u);break;case"string":m=u;break;default:throw new Error("Unrecognized type "+h.type)}return _[0]==239&&_[1]==187&&_[2]==191?m=na(m.slice(3)):h.type!="string"&&h.type!="buffer"&&h.codepage==65001?m=na(m):h.type=="binary"&&typeof Ii<"u",m.slice(0,19)=="socialcalc:version:"?qf.to_sheet(h.type=="string"?m:na(m),h):o(m,h)}function c(u,h){return es(l(u,h),h)}function d(u){for(var h=[],m=vt(u["!ref"]),_,x=Array.isArray(u),f=m.s.r;f<=m.e.r;++f){for(var p=[],I=m.s.c;I<=m.e.c;++I){var S=mt({r:f,c:I});if(_=x?(u[f]||[])[I]:u[S],!_||_.v==null){p.push(" ");continue}for(var w=(_.w||(yn(_),_.w)||"").slice(0,10);w.length<10;)w+=" ";p.push(w+(I===0?" ":""))}h.push(p.join(""))}return h.join(` +`)}return{to_workbook:c,to_sheet:l,from_sheet:d}}(),Ud=function(){function e(k,W,z){if(k){Tr(k,k.l||0);for(var D=z.Enum||Q;k.l=16&&k[14]==5&&k[15]===108)throw new Error("Unsupported Works 3 for Mac file");if(k[2]==2)z.Enum=Q,e(k,function(le,fe,Re){switch(Re){case 0:z.vers=le,le>=4096&&(z.qpro=!0);break;case 6:we=le;break;case 204:le&&(K=le);break;case 222:K=le;break;case 15:case 51:z.qpro||(le[1].v=le[1].v.slice(1));case 13:case 14:case 16:Re==14&&(le[2]&112)==112&&(le[2]&15)>1&&(le[2]&15)<15&&(le[1].z=z.dateNF||jt[14],z.cellDates&&(le[1].t="d",le[1].v=ff(le[1].v))),z.qpro&&le[3]>Z&&(D["!ref"]=Ot(we),me[q]=D,ie.push(q),D=z.dense?[]:{},we={s:{r:0,c:0},e:{r:0,c:0}},Z=le[3],q=K||"Sheet"+(Z+1),K="");var Ue=z.dense?(D[le[0].r]||[])[le[0].c]:D[mt(le[0])];if(Ue){Ue.t=le[1].t,Ue.v=le[1].v,le[1].z!=null&&(Ue.z=le[1].z),le[1].f!=null&&(Ue.f=le[1].f);break}z.dense?(D[le[0].r]||(D[le[0].r]=[]),D[le[0].r][le[0].c]=le[1]):D[mt(le[0])]=le[1];break}},z);else if(k[2]==26||k[2]==14)z.Enum=de,k[2]==14&&(z.qpro=!0,k.l=0),e(k,function(le,fe,Re){switch(Re){case 204:q=le;break;case 22:le[1].v=le[1].v.slice(1);case 23:case 24:case 25:case 37:case 39:case 40:if(le[3]>Z&&(D["!ref"]=Ot(we),me[q]=D,ie.push(q),D=z.dense?[]:{},we={s:{r:0,c:0},e:{r:0,c:0}},Z=le[3],q="Sheet"+(Z+1)),He>0&&le[0].r>=He)break;z.dense?(D[le[0].r]||(D[le[0].r]=[]),D[le[0].r][le[0].c]=le[1]):D[mt(le[0])]=le[1],we.e.c=0&&ga(+z.codepage),z.type=="string")throw new Error("Cannot write WK1 to JS string");var D=xr(),q=vt(k["!ref"]),K=Array.isArray(k),Z=[];Oe(D,0,i(1030)),Oe(D,6,c(q));for(var me=Math.min(q.e.r,8191),ie=q.s.r;ie<=me;++ie)for(var he=Jt(ie),we=q.s.c;we<=q.e.c;++we){ie===q.s.r&&(Z[we]=sr(we));var He=Z[we]+he,ze=K?(k[ie]||[])[we]:k[He];if(!(!ze||ze.t=="z"))if(ze.t=="n")(ze.v|0)==ze.v&&ze.v>=-32768&&ze.v<=32767?Oe(D,13,_(ie,we,ze.v)):Oe(D,14,f(ie,we,ze.v));else{var gt=yn(ze);Oe(D,15,h(ie,we,gt.slice(0,239)))}}return Oe(D,1),D.end()}function s(k,W){var z=W||{};if(+z.codepage>=0&&ga(+z.codepage),z.type=="string")throw new Error("Cannot write WK3 to JS string");var D=xr();Oe(D,0,o(k));for(var q=0,K=0;q8191&&(z=8191),W.write_shift(2,z),W.write_shift(1,q),W.write_shift(1,D),W.write_shift(2,0),W.write_shift(2,0),W.write_shift(1,1),W.write_shift(1,2),W.write_shift(4,0),W.write_shift(4,0),W}function l(k,W,z){var D={s:{c:0,r:0},e:{c:0,r:0}};return W==8&&z.qpro?(D.s.c=k.read_shift(1),k.l++,D.s.r=k.read_shift(2),D.e.c=k.read_shift(1),k.l++,D.e.r=k.read_shift(2),D):(D.s.c=k.read_shift(2),D.s.r=k.read_shift(2),W==12&&z.qpro&&(k.l+=2),D.e.c=k.read_shift(2),D.e.r=k.read_shift(2),W==12&&z.qpro&&(k.l+=2),D.s.c==65535&&(D.s.c=D.e.c=D.s.r=D.e.r=0),D)}function c(k){var W=pe(8);return W.write_shift(2,k.s.c),W.write_shift(2,k.s.r),W.write_shift(2,k.e.c),W.write_shift(2,k.e.r),W}function d(k,W,z){var D=[{c:0,r:0},{t:"n",v:0},0,0];return z.qpro&&z.vers!=20768?(D[0].c=k.read_shift(1),D[3]=k.read_shift(1),D[0].r=k.read_shift(2),k.l+=2):(D[2]=k.read_shift(1),D[0].c=k.read_shift(2),D[0].r=k.read_shift(2)),D}function u(k,W,z){var D=k.l+W,q=d(k,W,z);if(q[1].t="s",z.vers==20768){k.l++;var K=k.read_shift(1);return q[1].v=k.read_shift(K,"utf8"),q}return z.qpro&&k.l++,q[1].v=k.read_shift(D-k.l,"cstr"),q}function h(k,W,z){var D=pe(7+z.length);D.write_shift(1,255),D.write_shift(2,W),D.write_shift(2,k),D.write_shift(1,39);for(var q=0;q=128?95:K)}return D.write_shift(1,0),D}function m(k,W,z){var D=d(k,W,z);return D[1].v=k.read_shift(2,"i"),D}function _(k,W,z){var D=pe(7);return D.write_shift(1,255),D.write_shift(2,W),D.write_shift(2,k),D.write_shift(2,z,"i"),D}function x(k,W,z){var D=d(k,W,z);return D[1].v=k.read_shift(8,"f"),D}function f(k,W,z){var D=pe(13);return D.write_shift(1,255),D.write_shift(2,W),D.write_shift(2,k),D.write_shift(8,z,"f"),D}function p(k,W,z){var D=k.l+W,q=d(k,W,z);if(q[1].v=k.read_shift(8,"f"),z.qpro)k.l=D;else{var K=k.read_shift(2);v(k.slice(k.l,k.l+K),q),k.l+=K}return q}function I(k,W,z){var D=W&32768;return W&=-32769,W=(D?k:0)+(W>=8192?W-16384:W),(D?"":"$")+(z?sr(W):Jt(W))}var S={51:["FALSE",0],52:["TRUE",0],70:["LEN",1],80:["SUM",69],81:["AVERAGEA",69],82:["COUNTA",69],83:["MINA",69],84:["MAXA",69],111:["T",1]},w=["","","","","","","","","","+","-","*","/","^","=","<>","<=",">=","<",">","","","","","&","","","","","","",""];function v(k,W){Tr(k,0);for(var z=[],D=0,q="",K="",Z="",me="";k.lz.length){console.error("WK1 bad formula parse 0x"+ie.toString(16)+":|"+z.join("|")+"|");return}var ze=z.slice(-D);z.length-=D,z.push(S[ie][0]+"("+ze.join(",")+")")}else return ie<=7?console.error("WK1 invalid opcode "+ie.toString(16)):ie<=24?console.error("WK1 unsupported op "+ie.toString(16)):ie<=30?console.error("WK1 invalid opcode "+ie.toString(16)):ie<=115?console.error("WK1 unsupported function opcode "+ie.toString(16)):console.error("WK1 unrecognized opcode "+ie.toString(16))}}z.length==1?W[1].f=""+z[0]:console.error("WK1 bad formula parse |"+z.join("|")+"|")}function y(k){var W=[{c:0,r:0},{t:"n",v:0},0];return W[0].r=k.read_shift(2),W[3]=k[k.l++],W[0].c=k[k.l++],W}function R(k,W){var z=y(k);return z[1].t="s",z[1].v=k.read_shift(W-4,"cstr"),z}function j(k,W,z,D){var q=pe(6+D.length);q.write_shift(2,k),q.write_shift(1,z),q.write_shift(1,W),q.write_shift(1,39);for(var K=0;K=128?95:Z)}return q.write_shift(1,0),q}function T(k,W){var z=y(k);z[1].v=k.read_shift(2);var D=z[1].v>>1;if(z[1].v&1)switch(D&7){case 0:D=(D>>3)*5e3;break;case 1:D=(D>>3)*500;break;case 2:D=(D>>3)/20;break;case 3:D=(D>>3)/200;break;case 4:D=(D>>3)/2e3;break;case 5:D=(D>>3)/2e4;break;case 6:D=(D>>3)/16;break;case 7:D=(D>>3)/64;break}return z[1].v=D,z}function C(k,W){var z=y(k),D=k.read_shift(4),q=k.read_shift(4),K=k.read_shift(2);if(K==65535)return D===0&&q===3221225472?(z[1].t="e",z[1].v=15):D===0&&q===3489660928?(z[1].t="e",z[1].v=42):z[1].v=0,z;var Z=K&32768;return K=(K&32767)-16446,z[1].v=(1-Z*2)*(q*Math.pow(2,K+32)+D*Math.pow(2,K)),z}function F(k,W,z,D){var q=pe(14);if(q.write_shift(2,k),q.write_shift(1,z),q.write_shift(1,W),D==0)return q.write_shift(4,0),q.write_shift(4,0),q.write_shift(2,65535),q;var K=0,Z=0,me=0,ie=0;return D<0&&(K=1,D=-D),Z=Math.log2(D)|0,D/=Math.pow(2,Z-31),ie=D>>>0,ie&2147483648||(D/=2,++Z,ie=D>>>0),D-=ie,ie|=2147483648,ie>>>=0,D*=Math.pow(2,32),me=D>>>0,q.write_shift(4,me),q.write_shift(4,ie),Z+=16383+(K?32768:0),q.write_shift(2,Z),q}function P(k,W){var z=C(k);return k.l+=W-14,z}function V(k,W){var z=y(k),D=k.read_shift(4);return z[1].v=D>>6,z}function X(k,W){var z=y(k),D=k.read_shift(8,"f");return z[1].v=D,z}function _e(k,W){var z=X(k);return k.l+=W-10,z}function B(k,W){return k[k.l+W-1]==0?k.read_shift(W,"cstr"):""}function te(k,W){var z=k[k.l++];z>W-1&&(z=W-1);for(var D="";D.length127?95:q}return z[z.l++]=0,z}var Q={0:{n:"BOF",f:Hf},1:{n:"EOF"},2:{n:"CALCMODE"},3:{n:"CALCORDER"},4:{n:"SPLIT"},5:{n:"SYNC"},6:{n:"RANGE",f:l},7:{n:"WINDOW1"},8:{n:"COLW1"},9:{n:"WINTWO"},10:{n:"COLW2"},11:{n:"NAME"},12:{n:"BLANK"},13:{n:"INTEGER",f:m},14:{n:"NUMBER",f:x},15:{n:"LABEL",f:u},16:{n:"FORMULA",f:p},24:{n:"TABLE"},25:{n:"ORANGE"},26:{n:"PRANGE"},27:{n:"SRANGE"},28:{n:"FRANGE"},29:{n:"KRANGE1"},32:{n:"HRANGE"},35:{n:"KRANGE2"},36:{n:"PROTEC"},37:{n:"FOOTER"},38:{n:"HEADER"},39:{n:"SETUP"},40:{n:"MARGINS"},41:{n:"LABELFMT"},42:{n:"TITLES"},43:{n:"SHEETJS"},45:{n:"GRAPH"},46:{n:"NGRAPH"},47:{n:"CALCCOUNT"},48:{n:"UNFORMATTED"},49:{n:"CURSORW12"},50:{n:"WINDOW"},51:{n:"STRING",f:u},55:{n:"PASSWORD"},56:{n:"LOCKED"},60:{n:"QUERY"},61:{n:"QUERYNAME"},62:{n:"PRINT"},63:{n:"PRINTNAME"},64:{n:"GRAPH2"},65:{n:"GRAPHNAME"},66:{n:"ZOOM"},67:{n:"SYMSPLIT"},68:{n:"NSROWS"},69:{n:"NSCOLS"},70:{n:"RULER"},71:{n:"NNAME"},72:{n:"ACOMM"},73:{n:"AMACRO"},74:{n:"PARSE"},102:{n:"PRANGES??"},103:{n:"RRANGES??"},104:{n:"FNAME??"},105:{n:"MRANGES??"},204:{n:"SHEETNAMECS",f:B},222:{n:"SHEETNAMELP",f:te},65535:{n:""}},de={0:{n:"BOF"},1:{n:"EOF"},2:{n:"PASSWORD"},3:{n:"CALCSET"},4:{n:"WINDOWSET"},5:{n:"SHEETCELLPTR"},6:{n:"SHEETLAYOUT"},7:{n:"COLUMNWIDTH"},8:{n:"HIDDENCOLUMN"},9:{n:"USERRANGE"},10:{n:"SYSTEMRANGE"},11:{n:"ZEROFORCE"},12:{n:"SORTKEYDIR"},13:{n:"FILESEAL"},14:{n:"DATAFILLNUMS"},15:{n:"PRINTMAIN"},16:{n:"PRINTSTRING"},17:{n:"GRAPHMAIN"},18:{n:"GRAPHSTRING"},19:{n:"??"},20:{n:"ERRCELL"},21:{n:"NACELL"},22:{n:"LABEL16",f:R},23:{n:"NUMBER17",f:C},24:{n:"NUMBER18",f:T},25:{n:"FORMULA19",f:P},26:{n:"FORMULA1A"},27:{n:"XFORMAT",f:G},28:{n:"DTLABELMISC"},29:{n:"DTLABELCELL"},30:{n:"GRAPHWINDOW"},31:{n:"CPA"},32:{n:"LPLAUTO"},33:{n:"QUERY"},34:{n:"HIDDENSHEET"},35:{n:"??"},37:{n:"NUMBER25",f:V},38:{n:"??"},39:{n:"NUMBER27",f:X},40:{n:"FORMULA28",f:_e},142:{n:"??"},147:{n:"??"},150:{n:"??"},151:{n:"??"},152:{n:"??"},153:{n:"??"},154:{n:"??"},155:{n:"??"},156:{n:"??"},163:{n:"??"},174:{n:"??"},175:{n:"??"},176:{n:"??"},177:{n:"??"},184:{n:"??"},185:{n:"??"},186:{n:"??"},187:{n:"??"},188:{n:"??"},195:{n:"??"},201:{n:"??"},204:{n:"SHEETNAMECS",f:B},205:{n:"??"},206:{n:"??"},207:{n:"??"},208:{n:"??"},256:{n:"??"},259:{n:"??"},260:{n:"??"},261:{n:"??"},262:{n:"??"},263:{n:"??"},265:{n:"??"},266:{n:"??"},267:{n:"??"},268:{n:"??"},270:{n:"??"},271:{n:"??"},384:{n:"??"},389:{n:"??"},390:{n:"??"},393:{n:"??"},396:{n:"??"},512:{n:"??"},514:{n:"??"},513:{n:"??"},516:{n:"??"},517:{n:"??"},640:{n:"??"},641:{n:"??"},642:{n:"??"},643:{n:"??"},644:{n:"??"},645:{n:"??"},646:{n:"??"},647:{n:"??"},648:{n:"??"},658:{n:"??"},659:{n:"??"},660:{n:"??"},661:{n:"??"},662:{n:"??"},665:{n:"??"},666:{n:"??"},768:{n:"??"},772:{n:"??"},1537:{n:"SHEETINFOQP",f:J},1600:{n:"??"},1602:{n:"??"},1793:{n:"??"},1794:{n:"??"},1795:{n:"??"},1796:{n:"??"},1920:{n:"??"},2048:{n:"??"},2049:{n:"??"},2052:{n:"??"},2688:{n:"??"},10998:{n:"??"},12849:{n:"??"},28233:{n:"??"},28484:{n:"??"},65535:{n:""}};return{sheet_to_wk1:n,book_to_wk3:s,to_workbook:t}}(),Gy=/^\s|\s$|[\t\n\r]/;function Xf(e,t){if(!t.bookSST)return"";var r=[Rt];r[r.length]=Fe("sst",null,{xmlns:Ns[0],count:e.Count,uniqueCount:e.Unique});for(var n=0;n!=e.length;++n)if(e[n]!=null){var s=e[n],i="";s.r?i+=s.r:(i+=""),i+="",r[r.length]=i}return r.length>2&&(r[r.length]="",r[1]=r[1].replace("/>",">")),r.join("")}function Yy(e){return[e.read_shift(4),e.read_shift(4)]}function qy(e,t){return t||(t=pe(8)),t.write_shift(4,e.Count),t.write_shift(4,e.Unique),t}var Xy=Uv;function Jy(e){var t=xr();Se(t,159,qy(e));for(var r=0;r=0;--i)o=r[i],l=t&16384?1:0,c=t<<1&32767,d=l|c,t=d^o;return t^52811}var Qy=function(){function e(s,i){switch(i.type){case"base64":return t(vn(s),i);case"binary":return t(s,i);case"buffer":return t(ct&&Buffer.isBuffer(s)?s.toString("binary"):Da(s),i);case"array":return t(fo(s),i)}throw new Error("Unrecognized type "+i.type)}function t(s,i){var o=i||{},l=o.dense?[]:{},c=s.match(/\\trowd.*?\\row\b/g);if(!c.length)throw new Error("RTF missing table");var d={s:{c:0,r:0},e:{c:0,r:c.length-1}};return c.forEach(function(u,h){Array.isArray(l)&&(l[h]=[]);for(var m=/\\\w+\b/g,_=0,x,f=-1;x=m.exec(u);){switch(x[0]){case"\\cell":var p=u.slice(_,m.lastIndex-x[0].length);if(p[0]==" "&&(p=p.slice(1)),++f,p.length){var I={v:p,t:"s"};Array.isArray(l)?l[h][f]=I:l[mt({r:h,c:f})]=I}break}_=m.lastIndex}f>d.e.c&&(d.e.c=f)}),l["!ref"]=Ot(d),l}function r(s,i){return es(e(s,i),i)}function n(s){for(var i=["{\\rtf1\\ansi"],o=vt(s["!ref"]),l,c=Array.isArray(s),d=o.s.r;d<=o.e.r;++d){i.push("\\trowd\\trautofit1");for(var u=o.s.c;u<=o.e.c;++u)i.push("\\cellx"+(u+1));for(i.push("\\pard\\intbl"),u=o.s.c;u<=o.e.c;++u){var h=mt({r:d,c:u});l=c?(s[d]||[])[u]:s[h],!(!l||l.v==null&&(!l.f||l.F))&&(i.push(" "+(l.w||(yn(l),l.w))),i.push("\\cell"))}i.push("\\pard\\intbl\\row")}return i.join("")+"}"}return{to_workbook:r,to_sheet:e,from_sheet:n}}();function zd(e){for(var t=0,r=1;t!=3;++t)r=r*256+(e[t]>255?255:e[t]<0?0:e[t]);return r.toString(16).toUpperCase().slice(1)}var ew=6,gn=ew;function Ni(e){return Math.floor((e+Math.round(128/gn)/256)*gn)}function Li(e){return Math.floor((e-5)/gn*100+.5)/100}function ml(e){return Math.round((e*gn+5)/gn*256)/256}function ql(e){e.width?(e.wpx=Ni(e.width),e.wch=Li(e.wpx),e.MDW=gn):e.wpx?(e.wch=Li(e.wpx),e.width=ml(e.wch),e.MDW=gn):typeof e.wch=="number"&&(e.width=ml(e.wch),e.wpx=Ni(e.width),e.MDW=gn),e.customWidth&&delete e.customWidth}var tw=96,Zf=tw;function Mi(e){return e*96/Zf}function Qf(e){return e*Zf/96}function rw(e){var t=[""];return[[5,8],[23,26],[41,44],[50,392]].forEach(function(r){for(var n=r[0];n<=r[1];++n)e[n]!=null&&(t[t.length]=Fe("numFmt",null,{numFmtId:n,formatCode:ht(e[n])}))}),t.length===1?"":(t[t.length]="",t[0]=Fe("numFmts",null,{count:t.length-2}).replace("/>",">"),t.join(""))}function nw(e){var t=[];return t[t.length]=Fe("cellXfs",null),e.forEach(function(r){t[t.length]=Fe("xf",null,r)}),t[t.length]="",t.length===2?"":(t[0]=Fe("cellXfs",null,{count:t.length-2}).replace("/>",">"),t.join(""))}function eh(e,t){var r=[Rt,Fe("styleSheet",null,{xmlns:Ns[0],"xmlns:vt":Ut.vt})],n;return e.SSF&&(n=rw(e.SSF))!=null&&(r[r.length]=n),r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',(n=nw(t.cellXfs))&&(r[r.length]=n),r[r.length]='',r[r.length]='',r[r.length]='',r.length>2&&(r[r.length]="",r[1]=r[1].replace("/>",">")),r.join("")}function sw(e,t){var r=e.read_shift(2),n=ar(e);return[r,n]}function aw(e,t,r){r||(r=pe(6+4*t.length)),r.write_shift(2,e),$t(t,r);var n=r.length>r.l?r.slice(0,r.l):r;return r.l==null&&(r.l=r.length),n}function iw(e,t,r){var n={};n.sz=e.read_shift(2)/20;var s=Gv(e);s.fItalic&&(n.italic=1),s.fCondense&&(n.condense=1),s.fExtend&&(n.extend=1),s.fShadow&&(n.shadow=1),s.fOutline&&(n.outline=1),s.fStrikeout&&(n.strike=1);var i=e.read_shift(2);switch(i===700&&(n.bold=1),e.read_shift(2)){case 1:n.vertAlign="superscript";break;case 2:n.vertAlign="subscript";break}var o=e.read_shift(1);o!=0&&(n.underline=o);var l=e.read_shift(1);l>0&&(n.family=l);var c=e.read_shift(1);switch(c>0&&(n.charset=c),e.l++,n.color=Kv(e),e.read_shift(1)){case 1:n.scheme="major";break;case 2:n.scheme="minor";break}return n.name=ar(e),n}function ow(e,t){t||(t=pe(25+4*32)),t.write_shift(2,e.sz*20),Yv(e,t),t.write_shift(2,e.bold?700:400);var r=0;e.vertAlign=="superscript"?r=1:e.vertAlign=="subscript"&&(r=2),t.write_shift(2,r),t.write_shift(1,e.underline||0),t.write_shift(1,e.family||0),t.write_shift(1,e.charset||0),t.write_shift(1,0),Di(e.color,t);var n=0;return n=2,t.write_shift(1,n),$t(e.name,t),t.length>t.l?t.slice(0,t.l):t}var lw=["none","solid","mediumGray","darkGray","lightGray","darkHorizontal","darkVertical","darkDown","darkUp","darkGrid","darkTrellis","lightHorizontal","lightVertical","lightDown","lightUp","lightGrid","lightTrellis","gray125","gray0625"],Mo,cw=an;function $d(e,t){t||(t=pe(4*3+8*7+16*1)),Mo||(Mo=Ll(lw));var r=Mo[e.patternType];r==null&&(r=40),t.write_shift(4,r);var n=0;if(r!=40)for(Di({auto:1},t),Di({auto:1},t);n<12;++n)t.write_shift(4,0);else{for(;n<4;++n)t.write_shift(4,0);for(;n<12;++n)t.write_shift(4,0)}return t.length>t.l?t.slice(0,t.l):t}function dw(e,t){var r=e.l+t,n=e.read_shift(2),s=e.read_shift(2);return e.l=r,{ixfe:n,numFmtId:s}}function th(e,t,r){r||(r=pe(16)),r.write_shift(2,t||0),r.write_shift(2,e.numFmtId||0),r.write_shift(2,0),r.write_shift(2,0),r.write_shift(2,0),r.write_shift(1,0),r.write_shift(1,0);var n=0;return r.write_shift(1,n),r.write_shift(1,0),r.write_shift(1,0),r.write_shift(1,0),r}function Ws(e,t){return t||(t=pe(10)),t.write_shift(1,0),t.write_shift(1,0),t.write_shift(4,0),t.write_shift(4,0),t}var uw=an;function fw(e,t){return t||(t=pe(51)),t.write_shift(1,0),Ws(null,t),Ws(null,t),Ws(null,t),Ws(null,t),Ws(null,t),t.length>t.l?t.slice(0,t.l):t}function hw(e,t){return t||(t=pe(12+4*10)),t.write_shift(4,e.xfId),t.write_shift(2,1),t.write_shift(1,0),t.write_shift(1,0),Ri(e.name||"",t),t.length>t.l?t.slice(0,t.l):t}function mw(e,t,r){var n=pe(2052);return n.write_shift(4,e),Ri(t,n),Ri(r,n),n.length>n.l?n.slice(0,n.l):n}function xw(e,t){if(t){var r=0;[[5,8],[23,26],[41,44],[50,392]].forEach(function(n){for(var s=n[0];s<=n[1];++s)t[s]!=null&&++r}),r!=0&&(Se(e,615,Vr(r)),[[5,8],[23,26],[41,44],[50,392]].forEach(function(n){for(var s=n[0];s<=n[1];++s)t[s]!=null&&Se(e,44,aw(s,t[s]))}),Se(e,616))}}function pw(e){var t=1;Se(e,611,Vr(t)),Se(e,43,ow({sz:12,color:{theme:1},name:"Calibri",family:2})),Se(e,612)}function gw(e){var t=2;Se(e,603,Vr(t)),Se(e,45,$d({patternType:"none"})),Se(e,45,$d({patternType:"gray125"})),Se(e,604)}function _w(e){var t=1;Se(e,613,Vr(t)),Se(e,46,fw()),Se(e,614)}function vw(e){var t=1;Se(e,626,Vr(t)),Se(e,47,th({numFmtId:0},65535)),Se(e,627)}function yw(e,t){Se(e,617,Vr(t.length)),t.forEach(function(r){Se(e,47,th(r,0))}),Se(e,618)}function ww(e){var t=1;Se(e,619,Vr(t)),Se(e,48,hw({xfId:0,name:"Normal"})),Se(e,620)}function Sw(e){var t=0;Se(e,505,Vr(t)),Se(e,506)}function Tw(e){var t=0;Se(e,508,mw(t,"TableStyleMedium9","PivotStyleMedium4")),Se(e,509)}function bw(e,t){var r=xr();return Se(r,278),xw(r,e.SSF),pw(r),gw(r),_w(r),vw(r),yw(r,t.cellXfs),ww(r),Sw(r),Tw(r),Se(r,279),r.end()}function rh(e,t){if(t&&t.themeXLSX)return t.themeXLSX;if(e&&typeof e.raw=="string")return e.raw;var r=[Rt];return r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]='',r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]='',r[r.length]="",r[r.length]="",r[r.length]="",r[r.length]="",r.join("")}function Ew(e,t){return{flags:e.read_shift(4),version:e.read_shift(4),name:ar(e)}}function jw(e){var t=pe(12+2*e.name.length);return t.write_shift(4,e.flags),t.write_shift(4,e.version),$t(e.name,t),t.slice(0,t.l)}function Iw(e){for(var t=[],r=e.read_shift(4);r-- >0;)t.push([e.read_shift(4),e.read_shift(4)]);return t}function Cw(e){var t=pe(4+8*e.length);t.write_shift(4,e.length);for(var r=0;r + + + + + + + + + + + + + + + + + +`),e.join("")}function Rw(e){var t={};t.i=e.read_shift(4);var r={};r.r=e.read_shift(4),r.c=e.read_shift(4),t.r=mt(r);var n=e.read_shift(1);return n&2&&(t.l="1"),n&8&&(t.a="1"),t}var _s=1024;function sh(e,t){for(var r=[21600,21600],n=["m0,0l0",r[1],r[0],r[1],r[0],"0xe"].join(","),s=[Fe("xml",null,{"xmlns:v":br.v,"xmlns:o":br.o,"xmlns:x":br.x,"xmlns:mv":br.mv}).replace(/\/>/,">"),Fe("o:shapelayout",Fe("o:idmap",null,{"v:ext":"edit",data:e}),{"v:ext":"edit"}),Fe("v:shapetype",[Fe("v:stroke",null,{joinstyle:"miter"}),Fe("v:path",null,{gradientshapeok:"t","o:connecttype":"rect"})].join(""),{id:"_x0000_t202","o:spt":202,coordsize:r.join(","),path:n})];_s",d,Fe("v:shadow",null,u),Fe("v:path",null,{"o:connecttype":"none"}),'
','',"","",qt("x:Anchor",[o.c+1,0,o.r+1,0,o.c+3,20,o.r+5,20].join(",")),qt("x:AutoFill","False"),qt("x:Row",String(o.r)),qt("x:Column",String(o.c)),i[1].hidden?"":"","",""])}),s.push(""),s.join("")}function ah(e){var t=[Rt,Fe("comments",null,{xmlns:Ns[0]})],r=[];return t.push(""),e.forEach(function(n){n[1].forEach(function(s){var i=ht(s.a);r.indexOf(i)==-1&&(r.push(i),t.push(""+i+"")),s.T&&s.ID&&r.indexOf("tc="+s.ID)==-1&&(r.push("tc="+s.ID),t.push("tc="+s.ID+""))})}),r.length==0&&(r.push("SheetJ5"),t.push("SheetJ5")),t.push(""),t.push(""),e.forEach(function(n){var s=0,i=[];if(n[1][0]&&n[1][0].T&&n[1][0].ID?s=r.indexOf("tc="+n[1][0].ID):n[1].forEach(function(c){c.a&&(s=r.indexOf(ht(c.a))),i.push(c.t||"")}),t.push(''),i.length<=1)t.push(qt("t",ht(i[0]||"")));else{for(var o=`Comment: + `+i[0]+` +`,l=1;l")}),t.push(""),t.length>2&&(t[t.length]="",t[1]=t[1].replace("/>",">")),t.join("")}function Dw(e,t,r){var n=[Rt,Fe("ThreadedComments",null,{xmlns:Ut.TCMNT}).replace(/[\/]>/,">")];return e.forEach(function(s){var i="";(s[1]||[]).forEach(function(o,l){if(!o.T){delete o.ID;return}o.a&&t.indexOf(o.a)==-1&&t.push(o.a);var c={ref:s[0],id:"{54EE7951-7262-4200-6969-"+("000000000000"+r.tcid++).slice(-12)+"}"};l==0?i=c.id:c.parentId=i,o.ID=c.id,o.a&&(c.personId="{54EE7950-7262-4200-6969-"+("000000000000"+t.indexOf(o.a)).slice(-12)+"}"),n.push(Fe("threadedComment",qt("text",o.t||""),c))})}),n.push(""),n.join("")}function Pw(e){var t=[Rt,Fe("personList",null,{xmlns:Ut.TCMNT,"xmlns:x":Ns[0]}).replace(/[\/]>/,">")];return e.forEach(function(r,n){t.push(Fe("person",null,{displayName:r,id:"{54EE7950-7262-4200-6969-"+("000000000000"+n).slice(-12)+"}",userId:r,providerId:"None"}))}),t.push(""),t.join("")}function Nw(e){var t={};t.iauthor=e.read_shift(4);var r=ss(e);return t.rfx=r.s,t.ref=mt(r.s),e.l+=16,t}function Lw(e,t){return t==null&&(t=pe(36)),t.write_shift(4,e[1].iauthor),Ms(e[0],t),t.write_shift(4,0),t.write_shift(4,0),t.write_shift(4,0),t.write_shift(4,0),t}var Mw=ar;function Bw(e){return $t(e.slice(0,54))}function Uw(e){var t=xr(),r=[];return Se(t,628),Se(t,630),e.forEach(function(n){n[1].forEach(function(s){r.indexOf(s.a)>-1||(r.push(s.a.slice(0,54)),Se(t,632,Bw(s.a)))})}),Se(t,631),Se(t,633),e.forEach(function(n){n[1].forEach(function(s){s.iauthor=r.indexOf(s.a);var i={s:zt(n[0]),e:zt(n[0])};Se(t,635,Lw([i,s])),s.t&&s.t.length>0&&Se(t,637,$v(s)),Se(t,636),delete s.iauthor})}),Se(t,634),Se(t,629),t.end()}function zw(e,t){t.FullPaths.forEach(function(r,n){if(n!=0){var s=r.replace(/[^\/]*[\/]/,"/_VBA_PROJECT_CUR/");s.slice(-1)!=="/"&&xt.utils.cfb_add(e,s,t.FileIndex[n].content)}})}var ih=["xlsb","xlsm","xlam","biff8","xla"],$w=function(){var e=/(^|[^A-Za-z_])R(\[?-?\d+\]|[1-9]\d*|)C(\[?-?\d+\]|[1-9]\d*|)(?![A-Za-z0-9_])/g,t={r:0,c:0};function r(n,s,i,o){var l=!1,c=!1;i.length==0?c=!0:i.charAt(0)=="["&&(c=!0,i=i.slice(1,-1)),o.length==0?l=!0:o.charAt(0)=="["&&(l=!0,o=o.slice(1,-1));var d=i.length>0?parseInt(i,10)|0:0,u=o.length>0?parseInt(o,10)|0:0;return l?u+=t.c:--u,c?d+=t.r:--d,s+(l?"":"$")+sr(u)+(c?"":"$")+Jt(d)}return function(s,i){return t=i,s.replace(e,r)}}(),Xl=/(^|[^._A-Z0-9])([$]?)([A-Z]{1,2}|[A-W][A-Z]{2}|X[A-E][A-Z]|XF[A-D])([$]?)(10[0-3]\d{4}|104[0-7]\d{3}|1048[0-4]\d{2}|10485[0-6]\d|104857[0-6]|[1-9]\d{0,5})(?![_.\(A-Za-z0-9])/g,Jl=function(){return function(t,r){return t.replace(Xl,function(n,s,i,o,l,c){var d=Hl(o)-(i?0:r.c),u=Wl(c)-(l?0:r.r),h=u==0?"":l?u+1:"["+u+"]",m=d==0?"":i?d+1:"["+d+"]";return s+"R"+h+"C"+m})}}();function Ww(e,t){return e.replace(Xl,function(r,n,s,i,o,l){return n+(s=="$"?s+i:sr(Hl(i)+t.c))+(o=="$"?o+l:Jt(Wl(l)+t.r))})}function Hw(e){return e.length!=1}function Ft(e){e.l+=1}function Nn(e,t){var r=e.read_shift(2);return[r&16383,r>>14&1,r>>15&1]}function oh(e,t,r){var n=2;if(r){if(r.biff>=2&&r.biff<=5)return lh(e);r.biff==12&&(n=4)}var s=e.read_shift(n),i=e.read_shift(n),o=Nn(e),l=Nn(e);return{s:{r:s,c:o[0],cRel:o[1],rRel:o[2]},e:{r:i,c:l[0],cRel:l[1],rRel:l[2]}}}function lh(e){var t=Nn(e),r=Nn(e),n=e.read_shift(1),s=e.read_shift(1);return{s:{r:t[0],c:n,cRel:t[1],rRel:t[2]},e:{r:r[0],c:s,cRel:r[1],rRel:r[2]}}}function Vw(e,t,r){if(r.biff<8)return lh(e);var n=e.read_shift(r.biff==12?4:2),s=e.read_shift(r.biff==12?4:2),i=Nn(e),o=Nn(e);return{s:{r:n,c:i[0],cRel:i[1],rRel:i[2]},e:{r:s,c:o[0],cRel:o[1],rRel:o[2]}}}function ch(e,t,r){if(r&&r.biff>=2&&r.biff<=5)return Kw(e);var n=e.read_shift(r&&r.biff==12?4:2),s=Nn(e);return{r:n,c:s[0],cRel:s[1],rRel:s[2]}}function Kw(e){var t=Nn(e),r=e.read_shift(1);return{r:t[0],c:r,cRel:t[1],rRel:t[2]}}function Gw(e){var t=e.read_shift(2),r=e.read_shift(2);return{r:t,c:r&255,fQuoted:!!(r&16384),cRel:r>>15,rRel:r>>15}}function Yw(e,t,r){var n=r&&r.biff?r.biff:8;if(n>=2&&n<=5)return qw(e);var s=e.read_shift(n>=12?4:2),i=e.read_shift(2),o=(i&16384)>>14,l=(i&32768)>>15;if(i&=16383,l==1)for(;s>524287;)s-=1048576;if(o==1)for(;i>8191;)i=i-16384;return{r:s,c:i,cRel:o,rRel:l}}function qw(e){var t=e.read_shift(2),r=e.read_shift(1),n=(t&32768)>>15,s=(t&16384)>>14;return t&=16383,n==1&&t>=8192&&(t=t-16384),s==1&&r>=128&&(r=r-256),{r:t,c:r,cRel:s,rRel:n}}function Xw(e,t,r){var n=(e[e.l++]&96)>>5,s=oh(e,r.biff>=2&&r.biff<=5?6:8,r);return[n,s]}function Jw(e,t,r){var n=(e[e.l++]&96)>>5,s=e.read_shift(2,"i"),i=8;if(r)switch(r.biff){case 5:e.l+=12,i=6;break;case 12:i=12;break}var o=oh(e,i,r);return[n,s,o]}function Zw(e,t,r){var n=(e[e.l++]&96)>>5;return e.l+=r&&r.biff>8?12:r.biff<8?6:8,[n]}function Qw(e,t,r){var n=(e[e.l++]&96)>>5,s=e.read_shift(2),i=8;if(r)switch(r.biff){case 5:e.l+=12,i=6;break;case 12:i=12;break}return e.l+=i,[n,s]}function e2(e,t,r){var n=(e[e.l++]&96)>>5,s=Vw(e,t-1,r);return[n,s]}function t2(e,t,r){var n=(e[e.l++]&96)>>5;return e.l+=r.biff==2?6:r.biff==12?14:7,[n]}function Wd(e){var t=e[e.l+1]&1,r=1;return e.l+=4,[t,r]}function r2(e,t,r){e.l+=2;for(var n=e.read_shift(r&&r.biff==2?1:2),s=[],i=0;i<=n;++i)s.push(e.read_shift(r&&r.biff==2?1:2));return s}function n2(e,t,r){var n=e[e.l+1]&255?1:0;return e.l+=2,[n,e.read_shift(r&&r.biff==2?1:2)]}function s2(e,t,r){var n=e[e.l+1]&255?1:0;return e.l+=2,[n,e.read_shift(r&&r.biff==2?1:2)]}function a2(e){var t=e[e.l+1]&255?1:0;return e.l+=2,[t,e.read_shift(2)]}function i2(e,t,r){var n=e[e.l+1]&255?1:0;return e.l+=r&&r.biff==2?3:4,[n]}function dh(e){var t=e.read_shift(1),r=e.read_shift(1);return[t,r]}function o2(e){return e.read_shift(2),dh(e)}function l2(e){return e.read_shift(2),dh(e)}function c2(e,t,r){var n=(e[e.l]&96)>>5;e.l+=1;var s=ch(e,0,r);return[n,s]}function d2(e,t,r){var n=(e[e.l]&96)>>5;e.l+=1;var s=Yw(e,0,r);return[n,s]}function u2(e,t,r){var n=(e[e.l]&96)>>5;e.l+=1;var s=e.read_shift(2);r&&r.biff==5&&(e.l+=12);var i=ch(e,0,r);return[n,s,i]}function f2(e,t,r){var n=(e[e.l]&96)>>5;e.l+=1;var s=e.read_shift(r&&r.biff<=3?1:2);return[fS[s],hh[s],n]}function h2(e,t,r){var n=e[e.l++],s=e.read_shift(1),i=r&&r.biff<=3?[n==88?-1:0,e.read_shift(1)]:m2(e);return[s,(i[0]===0?hh:uS)[i[1]]]}function m2(e){return[e[e.l+1]>>7,e.read_shift(2)&32767]}function x2(e,t,r){e.l+=r&&r.biff==2?3:4}function p2(e,t,r){if(e.l++,r&&r.biff==12)return[e.read_shift(4,"i"),0];var n=e.read_shift(2),s=e.read_shift(r&&r.biff==2?1:2);return[n,s]}function g2(e){return e.l++,La[e.read_shift(1)]}function _2(e){return e.l++,e.read_shift(2)}function v2(e){return e.l++,e.read_shift(1)!==0}function y2(e){return e.l++,Bs(e)}function w2(e,t,r){return e.l++,Kf(e,t-1,r)}function S2(e,t){var r=[e.read_shift(1)];if(t==12)switch(r[0]){case 2:r[0]=4;break;case 4:r[0]=16;break;case 0:r[0]=1;break;case 1:r[0]=2;break}switch(r[0]){case 4:r[1]=uy(e,1)?"TRUE":"FALSE",t!=12&&(e.l+=7);break;case 37:case 16:r[1]=La[e[e.l]],e.l+=t==12?4:8;break;case 0:e.l+=8;break;case 1:r[1]=Bs(e);break;case 2:r[1]=xy(e,0,{biff:t>0&&t<8?2:t});break;default:throw new Error("Bad SerAr: "+r[0])}return r}function T2(e,t,r){for(var n=e.read_shift(r.biff==12?4:2),s=[],i=0;i!=n;++i)s.push((r.biff==12?ss:_y)(e));return s}function b2(e,t,r){var n=0,s=0;r.biff==12?(n=e.read_shift(4),s=e.read_shift(4)):(s=1+e.read_shift(1),n=1+e.read_shift(2)),r.biff>=2&&r.biff<8&&(--n,--s==0&&(s=256));for(var i=0,o=[];i!=n&&(o[i]=[]);++i)for(var l=0;l!=s;++l)o[i][l]=S2(e,r.biff);return o}function E2(e,t,r){var n=e.read_shift(1)>>>5&3,s=!r||r.biff>=8?4:2,i=e.read_shift(s);switch(r.biff){case 2:e.l+=5;break;case 3:case 4:e.l+=8;break;case 5:e.l+=12;break}return[n,0,i]}function j2(e,t,r){if(r.biff==5)return I2(e);var n=e.read_shift(1)>>>5&3,s=e.read_shift(2),i=e.read_shift(4);return[n,s,i]}function I2(e){var t=e.read_shift(1)>>>5&3,r=e.read_shift(2,"i");e.l+=8;var n=e.read_shift(2);return e.l+=12,[t,r,n]}function C2(e,t,r){var n=e.read_shift(1)>>>5&3;e.l+=r&&r.biff==2?3:4;var s=e.read_shift(r&&r.biff==2?1:2);return[n,s]}function A2(e,t,r){var n=e.read_shift(1)>>>5&3,s=e.read_shift(r&&r.biff==2?1:2);return[n,s]}function k2(e,t,r){var n=e.read_shift(1)>>>5&3;return e.l+=4,r.biff<8&&e.l--,r.biff==12&&(e.l+=2),[n]}function F2(e,t,r){var n=(e[e.l++]&96)>>5,s=e.read_shift(2),i=4;if(r)switch(r.biff){case 5:i=15;break;case 12:i=6;break}return e.l+=i,[n,s]}var O2=an,R2=an,D2=an;function Ma(e,t,r){return e.l+=2,[Gw(e)]}function Zl(e){return e.l+=6,[]}var P2=Ma,N2=Zl,L2=Zl,M2=Ma;function uh(e){return e.l+=2,[Hf(e),e.read_shift(2)&1]}var B2=Ma,U2=uh,z2=Zl,$2=Ma,W2=Ma,H2=["Data","All","Headers","??","?Data2","??","?DataHeaders","??","Totals","??","??","??","?DataTotals","??","??","??","?Current"];function V2(e){e.l+=2;var t=e.read_shift(2),r=e.read_shift(2),n=e.read_shift(4),s=e.read_shift(2),i=e.read_shift(2),o=H2[r>>2&31];return{ixti:t,coltype:r&3,rt:o,idx:n,c:s,C:i}}function K2(e){return e.l+=2,[e.read_shift(4)]}function G2(e,t,r){return e.l+=5,e.l+=2,e.l+=r.biff==2?1:4,["PTGSHEET"]}function Y2(e,t,r){return e.l+=r.biff==2?4:5,["PTGENDSHEET"]}function q2(e){var t=e.read_shift(1)>>>5&3,r=e.read_shift(2);return[t,r]}function X2(e){var t=e.read_shift(1)>>>5&3,r=e.read_shift(2);return[t,r]}function J2(e){return e.l+=4,[0,0]}var Hd={1:{n:"PtgExp",f:p2},2:{n:"PtgTbl",f:D2},3:{n:"PtgAdd",f:Ft},4:{n:"PtgSub",f:Ft},5:{n:"PtgMul",f:Ft},6:{n:"PtgDiv",f:Ft},7:{n:"PtgPower",f:Ft},8:{n:"PtgConcat",f:Ft},9:{n:"PtgLt",f:Ft},10:{n:"PtgLe",f:Ft},11:{n:"PtgEq",f:Ft},12:{n:"PtgGe",f:Ft},13:{n:"PtgGt",f:Ft},14:{n:"PtgNe",f:Ft},15:{n:"PtgIsect",f:Ft},16:{n:"PtgUnion",f:Ft},17:{n:"PtgRange",f:Ft},18:{n:"PtgUplus",f:Ft},19:{n:"PtgUminus",f:Ft},20:{n:"PtgPercent",f:Ft},21:{n:"PtgParen",f:Ft},22:{n:"PtgMissArg",f:Ft},23:{n:"PtgStr",f:w2},26:{n:"PtgSheet",f:G2},27:{n:"PtgEndSheet",f:Y2},28:{n:"PtgErr",f:g2},29:{n:"PtgBool",f:v2},30:{n:"PtgInt",f:_2},31:{n:"PtgNum",f:y2},32:{n:"PtgArray",f:t2},33:{n:"PtgFunc",f:f2},34:{n:"PtgFuncVar",f:h2},35:{n:"PtgName",f:E2},36:{n:"PtgRef",f:c2},37:{n:"PtgArea",f:Xw},38:{n:"PtgMemArea",f:C2},39:{n:"PtgMemErr",f:O2},40:{n:"PtgMemNoMem",f:R2},41:{n:"PtgMemFunc",f:A2},42:{n:"PtgRefErr",f:k2},43:{n:"PtgAreaErr",f:Zw},44:{n:"PtgRefN",f:d2},45:{n:"PtgAreaN",f:e2},46:{n:"PtgMemAreaN",f:q2},47:{n:"PtgMemNoMemN",f:X2},57:{n:"PtgNameX",f:j2},58:{n:"PtgRef3d",f:u2},59:{n:"PtgArea3d",f:Jw},60:{n:"PtgRefErr3d",f:F2},61:{n:"PtgAreaErr3d",f:Qw},255:{}},Z2={64:32,96:32,65:33,97:33,66:34,98:34,67:35,99:35,68:36,100:36,69:37,101:37,70:38,102:38,71:39,103:39,72:40,104:40,73:41,105:41,74:42,106:42,75:43,107:43,76:44,108:44,77:45,109:45,78:46,110:46,79:47,111:47,88:34,120:34,89:57,121:57,90:58,122:58,91:59,123:59,92:60,124:60,93:61,125:61},Q2={1:{n:"PtgElfLel",f:uh},2:{n:"PtgElfRw",f:$2},3:{n:"PtgElfCol",f:P2},6:{n:"PtgElfRwV",f:W2},7:{n:"PtgElfColV",f:M2},10:{n:"PtgElfRadical",f:B2},11:{n:"PtgElfRadicalS",f:z2},13:{n:"PtgElfColS",f:N2},15:{n:"PtgElfColSV",f:L2},16:{n:"PtgElfRadicalLel",f:U2},25:{n:"PtgList",f:V2},29:{n:"PtgSxName",f:K2},255:{}},eS={0:{n:"PtgAttrNoop",f:J2},1:{n:"PtgAttrSemi",f:i2},2:{n:"PtgAttrIf",f:s2},4:{n:"PtgAttrChoose",f:r2},8:{n:"PtgAttrGoto",f:n2},16:{n:"PtgAttrSum",f:x2},32:{n:"PtgAttrBaxcel",f:Wd},33:{n:"PtgAttrBaxcel",f:Wd},64:{n:"PtgAttrSpace",f:o2},65:{n:"PtgAttrSpaceSemi",f:l2},128:{n:"PtgAttrIfError",f:a2},255:{}};function tS(e,t,r,n){if(n.biff<8)return an(e,t);for(var s=e.l+t,i=[],o=0;o!==r.length;++o)switch(r[o][0]){case"PtgArray":r[o][1]=b2(e,0,n),i.push(r[o][1]);break;case"PtgMemArea":r[o][2]=T2(e,r[o][1],n),i.push(r[o][2]);break;case"PtgExp":n&&n.biff==12&&(r[o][1][1]=e.read_shift(4),i.push(r[o][1]));break;case"PtgList":case"PtgElfRadicalS":case"PtgElfColS":case"PtgElfColSV":throw"Unsupported "+r[o][0]}return t=s-e.l,t!==0&&i.push(an(e,t)),i}function rS(e,t,r){for(var n=e.l+t,s,i,o=[];n!=e.l;)t=n-e.l,i=e[e.l],s=Hd[i]||Hd[Z2[i]],(i===24||i===25)&&(s=(i===24?Q2:eS)[e[e.l+1]]),!s||!s.f?an(e,t):o.push([s.n,s.f(e,t,r)]);return o}function nS(e){for(var t=[],r=0;r=",PtgGt:">",PtgLe:"<=",PtgLt:"<",PtgMul:"*",PtgNe:"<>",PtgPower:"^",PtgSub:"-"};function aS(e,t){if(!e&&!(t&&t.biff<=5&&t.biff>=2))throw new Error("empty sheet name");return/[^\w\u4E00-\u9FFF\u3040-\u30FF]/.test(e)?"'"+e+"'":e}function fh(e,t,r){if(!e)return"SH33TJSERR0";if(r.biff>8&&(!e.XTI||!e.XTI[t]))return e.SheetNames[t];if(!e.XTI)return"SH33TJSERR6";var n=e.XTI[t];if(r.biff<8)return t>1e4&&(t-=65536),t<0&&(t=-t),t==0?"":e.XTI[t-1];if(!n)return"SH33TJSERR1";var s="";if(r.biff>8)switch(e[n[0]][0]){case 357:return s=n[1]==-1?"#REF":e.SheetNames[n[1]],n[1]==n[2]?s:s+":"+e.SheetNames[n[2]];case 358:return r.SID!=null?e.SheetNames[r.SID]:"SH33TJSSAME"+e[n[0]][0];case 355:default:return"SH33TJSSRC"+e[n[0]][0]}switch(e[n[0]][0][0]){case 1025:return s=n[1]==-1?"#REF":e.SheetNames[n[1]]||"SH33TJSERR3",n[1]==n[2]?s:s+":"+e.SheetNames[n[2]];case 14849:return e[n[0]].slice(1).map(function(i){return i.Name}).join(";;");default:return e[n[0]][0][3]?(s=n[1]==-1?"#REF":e[n[0]][0][3][n[1]]||"SH33TJSERR4",n[1]==n[2]?s:s+":"+e[n[0]][0][3][n[2]]):"SH33TJSERR2"}}function Vd(e,t,r){var n=fh(e,t,r);return n=="#REF"?n:aS(n,r)}function As(e,t,r,n,s){var i=s&&s.biff||8,o={s:{c:0,r:0}},l=[],c,d,u,h=0,m=0,_,x="";if(!e[0]||!e[0][0])return"";for(var f=-1,p="",I=0,S=e[0].length;I=0){switch(e[0][f][1][0]){case 0:p=Et(" ",e[0][f][1][1]);break;case 1:p=Et("\r",e[0][f][1][1]);break;default:if(p="",s.WTF)throw new Error("Unexpected PtgAttrSpaceType "+e[0][f][1][0])}d=d+p,f=-1}l.push(d+sS[w[0]]+c);break;case"PtgIsect":c=l.pop(),d=l.pop(),l.push(d+" "+c);break;case"PtgUnion":c=l.pop(),d=l.pop(),l.push(d+","+c);break;case"PtgRange":c=l.pop(),d=l.pop(),l.push(d+":"+c);break;case"PtgAttrChoose":break;case"PtgAttrGoto":break;case"PtgAttrIf":break;case"PtgAttrIfError":break;case"PtgRef":u=aa(w[1][1],o,s),l.push(ia(u,i));break;case"PtgRefN":u=r?aa(w[1][1],r,s):w[1][1],l.push(ia(u,i));break;case"PtgRef3d":h=w[1][1],u=aa(w[1][2],o,s),x=Vd(n,h,s),l.push(x+"!"+ia(u,i));break;case"PtgFunc":case"PtgFuncVar":var v=w[1][0],y=w[1][1];v||(v=0),v&=127;var R=v==0?[]:l.slice(-v);l.length-=v,y==="User"&&(y=R.shift()),l.push(y+"("+R.join(",")+")");break;case"PtgBool":l.push(w[1]?"TRUE":"FALSE");break;case"PtgInt":l.push(w[1]);break;case"PtgNum":l.push(String(w[1]));break;case"PtgStr":l.push('"'+w[1].replace(/"/g,'""')+'"');break;case"PtgErr":l.push(w[1]);break;case"PtgAreaN":_=Ad(w[1][1],r?{s:r}:o,s),l.push(No(_,s));break;case"PtgArea":_=Ad(w[1][1],o,s),l.push(No(_,s));break;case"PtgArea3d":h=w[1][1],_=w[1][2],x=Vd(n,h,s),l.push(x+"!"+No(_,s));break;case"PtgAttrSum":l.push("SUM("+l.pop()+")");break;case"PtgAttrBaxcel":case"PtgAttrSemi":break;case"PtgName":m=w[1][2];var j=(n.names||[])[m-1]||(n[0]||[])[m],T=j?j.Name:"SH33TJSNAME"+String(m);T&&T.slice(0,6)=="_xlfn."&&!s.xlfn&&(T=T.slice(6)),l.push(T);break;case"PtgNameX":var C=w[1][1];m=w[1][2];var F;if(s.biff<=5)C<0&&(C=-C),n[C]&&(F=n[C][m]);else{var P="";if(((n[C]||[])[0]||[])[0]==14849||(((n[C]||[])[0]||[])[0]==1025?n[C][m]&&n[C][m].itab>0&&(P=n.SheetNames[n[C][m].itab-1]+"!"):P=n.SheetNames[m-1]+"!"),n[C]&&n[C][m])P+=n[C][m].Name;else if(n[0]&&n[0][m])P+=n[0][m].Name;else{var V=(fh(n,C,s)||"").split(";;");V[m-1]?P=V[m-1]:P+="SH33TJSERRX"}l.push(P);break}F||(F={Name:"SH33TJSERRY"}),l.push(F.Name);break;case"PtgParen":var X="(",_e=")";if(f>=0){switch(p="",e[0][f][1][0]){case 2:X=Et(" ",e[0][f][1][1])+X;break;case 3:X=Et("\r",e[0][f][1][1])+X;break;case 4:_e=Et(" ",e[0][f][1][1])+_e;break;case 5:_e=Et("\r",e[0][f][1][1])+_e;break;default:if(s.WTF)throw new Error("Unexpected PtgAttrSpaceType "+e[0][f][1][0])}f=-1}l.push(X+l.pop()+_e);break;case"PtgRefErr":l.push("#REF!");break;case"PtgRefErr3d":l.push("#REF!");break;case"PtgExp":u={c:w[1][1],r:w[1][0]};var B={c:r.c,r:r.r};if(n.sharedf[mt(u)]){var te=n.sharedf[mt(u)];l.push(As(te,o,B,n,s))}else{var J=!1;for(c=0;c!=n.arrayf.length;++c)if(d=n.arrayf[c],!(u.cd[0].e.c)&&!(u.rd[0].e.r)){l.push(As(d[1],o,B,n,s)),J=!0;break}J||l.push(w[1])}break;case"PtgArray":l.push("{"+nS(w[1])+"}");break;case"PtgMemArea":break;case"PtgAttrSpace":case"PtgAttrSpaceSemi":f=I;break;case"PtgTbl":break;case"PtgMemErr":break;case"PtgMissArg":l.push("");break;case"PtgAreaErr":l.push("#REF!");break;case"PtgAreaErr3d":l.push("#REF!");break;case"PtgList":l.push("Table"+w[1].idx+"[#"+w[1].rt+"]");break;case"PtgMemAreaN":case"PtgMemNoMemN":case"PtgAttrNoop":case"PtgSheet":case"PtgEndSheet":break;case"PtgMemFunc":break;case"PtgMemNoMem":break;case"PtgElfCol":case"PtgElfColS":case"PtgElfColSV":case"PtgElfColV":case"PtgElfLel":case"PtgElfRadical":case"PtgElfRadicalLel":case"PtgElfRadicalS":case"PtgElfRw":case"PtgElfRwV":throw new Error("Unsupported ELFs");case"PtgSxName":throw new Error("Unrecognized Formula Token: "+String(w));default:throw new Error("Unrecognized Formula Token: "+String(w))}var G=["PtgAttrSpace","PtgAttrSpaceSemi","PtgAttrGoto"];if(s.biff!=3&&f>=0&&G.indexOf(e[0][I][0])==-1){w=e[0][f];var se=!0;switch(w[1][0]){case 4:se=!1;case 0:p=Et(" ",w[1][1]);break;case 5:se=!1;case 1:p=Et("\r",w[1][1]);break;default:if(p="",s.WTF)throw new Error("Unexpected PtgAttrSpaceType "+w[1][0])}l.push((se?p:"")+l.pop()+(se?"":p)),f=-1}}if(l.length>1&&s.WTF)throw new Error("bad formula stack");return l[0]}function iS(e){if(e==null){var t=pe(8);return t.write_shift(1,3),t.write_shift(1,0),t.write_shift(2,0),t.write_shift(2,0),t.write_shift(2,65535),t}else if(typeof e=="number")return Zn(e);return Zn(0)}function oS(e,t,r,n,s){var i=Qn(t,r,s),o=iS(e.v),l=pe(6),c=33;l.write_shift(2,c),l.write_shift(4,0);for(var d=pe(e.bf.length),u=0;u0?tS(e,i,s,r):null;return[s,o]}var lS=ho,mo=ho,cS=ho,dS=ho,uS={0:"BEEP",1:"OPEN",2:"OPEN.LINKS",3:"CLOSE.ALL",4:"SAVE",5:"SAVE.AS",6:"FILE.DELETE",7:"PAGE.SETUP",8:"PRINT",9:"PRINTER.SETUP",10:"QUIT",11:"NEW.WINDOW",12:"ARRANGE.ALL",13:"WINDOW.SIZE",14:"WINDOW.MOVE",15:"FULL",16:"CLOSE",17:"RUN",22:"SET.PRINT.AREA",23:"SET.PRINT.TITLES",24:"SET.PAGE.BREAK",25:"REMOVE.PAGE.BREAK",26:"FONT",27:"DISPLAY",28:"PROTECT.DOCUMENT",29:"PRECISION",30:"A1.R1C1",31:"CALCULATE.NOW",32:"CALCULATION",34:"DATA.FIND",35:"EXTRACT",36:"DATA.DELETE",37:"SET.DATABASE",38:"SET.CRITERIA",39:"SORT",40:"DATA.SERIES",41:"TABLE",42:"FORMAT.NUMBER",43:"ALIGNMENT",44:"STYLE",45:"BORDER",46:"CELL.PROTECTION",47:"COLUMN.WIDTH",48:"UNDO",49:"CUT",50:"COPY",51:"PASTE",52:"CLEAR",53:"PASTE.SPECIAL",54:"EDIT.DELETE",55:"INSERT",56:"FILL.RIGHT",57:"FILL.DOWN",61:"DEFINE.NAME",62:"CREATE.NAMES",63:"FORMULA.GOTO",64:"FORMULA.FIND",65:"SELECT.LAST.CELL",66:"SHOW.ACTIVE.CELL",67:"GALLERY.AREA",68:"GALLERY.BAR",69:"GALLERY.COLUMN",70:"GALLERY.LINE",71:"GALLERY.PIE",72:"GALLERY.SCATTER",73:"COMBINATION",74:"PREFERRED",75:"ADD.OVERLAY",76:"GRIDLINES",77:"SET.PREFERRED",78:"AXES",79:"LEGEND",80:"ATTACH.TEXT",81:"ADD.ARROW",82:"SELECT.CHART",83:"SELECT.PLOT.AREA",84:"PATTERNS",85:"MAIN.CHART",86:"OVERLAY",87:"SCALE",88:"FORMAT.LEGEND",89:"FORMAT.TEXT",90:"EDIT.REPEAT",91:"PARSE",92:"JUSTIFY",93:"HIDE",94:"UNHIDE",95:"WORKSPACE",96:"FORMULA",97:"FORMULA.FILL",98:"FORMULA.ARRAY",99:"DATA.FIND.NEXT",100:"DATA.FIND.PREV",101:"FORMULA.FIND.NEXT",102:"FORMULA.FIND.PREV",103:"ACTIVATE",104:"ACTIVATE.NEXT",105:"ACTIVATE.PREV",106:"UNLOCKED.NEXT",107:"UNLOCKED.PREV",108:"COPY.PICTURE",109:"SELECT",110:"DELETE.NAME",111:"DELETE.FORMAT",112:"VLINE",113:"HLINE",114:"VPAGE",115:"HPAGE",116:"VSCROLL",117:"HSCROLL",118:"ALERT",119:"NEW",120:"CANCEL.COPY",121:"SHOW.CLIPBOARD",122:"MESSAGE",124:"PASTE.LINK",125:"APP.ACTIVATE",126:"DELETE.ARROW",127:"ROW.HEIGHT",128:"FORMAT.MOVE",129:"FORMAT.SIZE",130:"FORMULA.REPLACE",131:"SEND.KEYS",132:"SELECT.SPECIAL",133:"APPLY.NAMES",134:"REPLACE.FONT",135:"FREEZE.PANES",136:"SHOW.INFO",137:"SPLIT",138:"ON.WINDOW",139:"ON.DATA",140:"DISABLE.INPUT",142:"OUTLINE",143:"LIST.NAMES",144:"FILE.CLOSE",145:"SAVE.WORKBOOK",146:"DATA.FORM",147:"COPY.CHART",148:"ON.TIME",149:"WAIT",150:"FORMAT.FONT",151:"FILL.UP",152:"FILL.LEFT",153:"DELETE.OVERLAY",155:"SHORT.MENUS",159:"SET.UPDATE.STATUS",161:"COLOR.PALETTE",162:"DELETE.STYLE",163:"WINDOW.RESTORE",164:"WINDOW.MAXIMIZE",166:"CHANGE.LINK",167:"CALCULATE.DOCUMENT",168:"ON.KEY",169:"APP.RESTORE",170:"APP.MOVE",171:"APP.SIZE",172:"APP.MINIMIZE",173:"APP.MAXIMIZE",174:"BRING.TO.FRONT",175:"SEND.TO.BACK",185:"MAIN.CHART.TYPE",186:"OVERLAY.CHART.TYPE",187:"SELECT.END",188:"OPEN.MAIL",189:"SEND.MAIL",190:"STANDARD.FONT",191:"CONSOLIDATE",192:"SORT.SPECIAL",193:"GALLERY.3D.AREA",194:"GALLERY.3D.COLUMN",195:"GALLERY.3D.LINE",196:"GALLERY.3D.PIE",197:"VIEW.3D",198:"GOAL.SEEK",199:"WORKGROUP",200:"FILL.GROUP",201:"UPDATE.LINK",202:"PROMOTE",203:"DEMOTE",204:"SHOW.DETAIL",206:"UNGROUP",207:"OBJECT.PROPERTIES",208:"SAVE.NEW.OBJECT",209:"SHARE",210:"SHARE.NAME",211:"DUPLICATE",212:"APPLY.STYLE",213:"ASSIGN.TO.OBJECT",214:"OBJECT.PROTECTION",215:"HIDE.OBJECT",216:"SET.EXTRACT",217:"CREATE.PUBLISHER",218:"SUBSCRIBE.TO",219:"ATTRIBUTES",220:"SHOW.TOOLBAR",222:"PRINT.PREVIEW",223:"EDIT.COLOR",224:"SHOW.LEVELS",225:"FORMAT.MAIN",226:"FORMAT.OVERLAY",227:"ON.RECALC",228:"EDIT.SERIES",229:"DEFINE.STYLE",240:"LINE.PRINT",243:"ENTER.DATA",249:"GALLERY.RADAR",250:"MERGE.STYLES",251:"EDITION.OPTIONS",252:"PASTE.PICTURE",253:"PASTE.PICTURE.LINK",254:"SPELLING",256:"ZOOM",259:"INSERT.OBJECT",260:"WINDOW.MINIMIZE",265:"SOUND.NOTE",266:"SOUND.PLAY",267:"FORMAT.SHAPE",268:"EXTEND.POLYGON",269:"FORMAT.AUTO",272:"GALLERY.3D.BAR",273:"GALLERY.3D.SURFACE",274:"FILL.AUTO",276:"CUSTOMIZE.TOOLBAR",277:"ADD.TOOL",278:"EDIT.OBJECT",279:"ON.DOUBLECLICK",280:"ON.ENTRY",281:"WORKBOOK.ADD",282:"WORKBOOK.MOVE",283:"WORKBOOK.COPY",284:"WORKBOOK.OPTIONS",285:"SAVE.WORKSPACE",288:"CHART.WIZARD",289:"DELETE.TOOL",290:"MOVE.TOOL",291:"WORKBOOK.SELECT",292:"WORKBOOK.ACTIVATE",293:"ASSIGN.TO.TOOL",295:"COPY.TOOL",296:"RESET.TOOL",297:"CONSTRAIN.NUMERIC",298:"PASTE.TOOL",302:"WORKBOOK.NEW",305:"SCENARIO.CELLS",306:"SCENARIO.DELETE",307:"SCENARIO.ADD",308:"SCENARIO.EDIT",309:"SCENARIO.SHOW",310:"SCENARIO.SHOW.NEXT",311:"SCENARIO.SUMMARY",312:"PIVOT.TABLE.WIZARD",313:"PIVOT.FIELD.PROPERTIES",314:"PIVOT.FIELD",315:"PIVOT.ITEM",316:"PIVOT.ADD.FIELDS",318:"OPTIONS.CALCULATION",319:"OPTIONS.EDIT",320:"OPTIONS.VIEW",321:"ADDIN.MANAGER",322:"MENU.EDITOR",323:"ATTACH.TOOLBARS",324:"VBAActivate",325:"OPTIONS.CHART",328:"VBA.INSERT.FILE",330:"VBA.PROCEDURE.DEFINITION",336:"ROUTING.SLIP",338:"ROUTE.DOCUMENT",339:"MAIL.LOGON",342:"INSERT.PICTURE",343:"EDIT.TOOL",344:"GALLERY.DOUGHNUT",350:"CHART.TREND",352:"PIVOT.ITEM.PROPERTIES",354:"WORKBOOK.INSERT",355:"OPTIONS.TRANSITION",356:"OPTIONS.GENERAL",370:"FILTER.ADVANCED",373:"MAIL.ADD.MAILER",374:"MAIL.DELETE.MAILER",375:"MAIL.REPLY",376:"MAIL.REPLY.ALL",377:"MAIL.FORWARD",378:"MAIL.NEXT.LETTER",379:"DATA.LABEL",380:"INSERT.TITLE",381:"FONT.PROPERTIES",382:"MACRO.OPTIONS",383:"WORKBOOK.HIDE",384:"WORKBOOK.UNHIDE",385:"WORKBOOK.DELETE",386:"WORKBOOK.NAME",388:"GALLERY.CUSTOM",390:"ADD.CHART.AUTOFORMAT",391:"DELETE.CHART.AUTOFORMAT",392:"CHART.ADD.DATA",393:"AUTO.OUTLINE",394:"TAB.ORDER",395:"SHOW.DIALOG",396:"SELECT.ALL",397:"UNGROUP.SHEETS",398:"SUBTOTAL.CREATE",399:"SUBTOTAL.REMOVE",400:"RENAME.OBJECT",412:"WORKBOOK.SCROLL",413:"WORKBOOK.NEXT",414:"WORKBOOK.PREV",415:"WORKBOOK.TAB.SPLIT",416:"FULL.SCREEN",417:"WORKBOOK.PROTECT",420:"SCROLLBAR.PROPERTIES",421:"PIVOT.SHOW.PAGES",422:"TEXT.TO.COLUMNS",423:"FORMAT.CHARTTYPE",424:"LINK.FORMAT",425:"TRACER.DISPLAY",430:"TRACER.NAVIGATE",431:"TRACER.CLEAR",432:"TRACER.ERROR",433:"PIVOT.FIELD.GROUP",434:"PIVOT.FIELD.UNGROUP",435:"CHECKBOX.PROPERTIES",436:"LABEL.PROPERTIES",437:"LISTBOX.PROPERTIES",438:"EDITBOX.PROPERTIES",439:"PIVOT.REFRESH",440:"LINK.COMBO",441:"OPEN.TEXT",442:"HIDE.DIALOG",443:"SET.DIALOG.FOCUS",444:"ENABLE.OBJECT",445:"PUSHBUTTON.PROPERTIES",446:"SET.DIALOG.DEFAULT",447:"FILTER",448:"FILTER.SHOW.ALL",449:"CLEAR.OUTLINE",450:"FUNCTION.WIZARD",451:"ADD.LIST.ITEM",452:"SET.LIST.ITEM",453:"REMOVE.LIST.ITEM",454:"SELECT.LIST.ITEM",455:"SET.CONTROL.VALUE",456:"SAVE.COPY.AS",458:"OPTIONS.LISTS.ADD",459:"OPTIONS.LISTS.DELETE",460:"SERIES.AXES",461:"SERIES.X",462:"SERIES.Y",463:"ERRORBAR.X",464:"ERRORBAR.Y",465:"FORMAT.CHART",466:"SERIES.ORDER",467:"MAIL.LOGOFF",468:"CLEAR.ROUTING.SLIP",469:"APP.ACTIVATE.MICROSOFT",470:"MAIL.EDIT.MAILER",471:"ON.SHEET",472:"STANDARD.WIDTH",473:"SCENARIO.MERGE",474:"SUMMARY.INFO",475:"FIND.FILE",476:"ACTIVE.CELL.FONT",477:"ENABLE.TIPWIZARD",478:"VBA.MAKE.ADDIN",480:"INSERTDATATABLE",481:"WORKGROUP.OPTIONS",482:"MAIL.SEND.MAILER",485:"AUTOCORRECT",489:"POST.DOCUMENT",491:"PICKLIST",493:"VIEW.SHOW",494:"VIEW.DEFINE",495:"VIEW.DELETE",509:"SHEET.BACKGROUND",510:"INSERT.MAP.OBJECT",511:"OPTIONS.MENONO",517:"MSOCHECKS",518:"NORMAL",519:"LAYOUT",520:"RM.PRINT.AREA",521:"CLEAR.PRINT.AREA",522:"ADD.PRINT.AREA",523:"MOVE.BRK",545:"HIDECURR.NOTE",546:"HIDEALL.NOTES",547:"DELETE.NOTE",548:"TRAVERSE.NOTES",549:"ACTIVATE.NOTES",620:"PROTECT.REVISIONS",621:"UNPROTECT.REVISIONS",647:"OPTIONS.ME",653:"WEB.PUBLISH",667:"NEWWEBQUERY",673:"PIVOT.TABLE.CHART",753:"OPTIONS.SAVE",755:"OPTIONS.SPELL",808:"HIDEALL.INKANNOTS"},hh={0:"COUNT",1:"IF",2:"ISNA",3:"ISERROR",4:"SUM",5:"AVERAGE",6:"MIN",7:"MAX",8:"ROW",9:"COLUMN",10:"NA",11:"NPV",12:"STDEV",13:"DOLLAR",14:"FIXED",15:"SIN",16:"COS",17:"TAN",18:"ATAN",19:"PI",20:"SQRT",21:"EXP",22:"LN",23:"LOG10",24:"ABS",25:"INT",26:"SIGN",27:"ROUND",28:"LOOKUP",29:"INDEX",30:"REPT",31:"MID",32:"LEN",33:"VALUE",34:"TRUE",35:"FALSE",36:"AND",37:"OR",38:"NOT",39:"MOD",40:"DCOUNT",41:"DSUM",42:"DAVERAGE",43:"DMIN",44:"DMAX",45:"DSTDEV",46:"VAR",47:"DVAR",48:"TEXT",49:"LINEST",50:"TREND",51:"LOGEST",52:"GROWTH",53:"GOTO",54:"HALT",55:"RETURN",56:"PV",57:"FV",58:"NPER",59:"PMT",60:"RATE",61:"MIRR",62:"IRR",63:"RAND",64:"MATCH",65:"DATE",66:"TIME",67:"DAY",68:"MONTH",69:"YEAR",70:"WEEKDAY",71:"HOUR",72:"MINUTE",73:"SECOND",74:"NOW",75:"AREAS",76:"ROWS",77:"COLUMNS",78:"OFFSET",79:"ABSREF",80:"RELREF",81:"ARGUMENT",82:"SEARCH",83:"TRANSPOSE",84:"ERROR",85:"STEP",86:"TYPE",87:"ECHO",88:"SET.NAME",89:"CALLER",90:"DEREF",91:"WINDOWS",92:"SERIES",93:"DOCUMENTS",94:"ACTIVE.CELL",95:"SELECTION",96:"RESULT",97:"ATAN2",98:"ASIN",99:"ACOS",100:"CHOOSE",101:"HLOOKUP",102:"VLOOKUP",103:"LINKS",104:"INPUT",105:"ISREF",106:"GET.FORMULA",107:"GET.NAME",108:"SET.VALUE",109:"LOG",110:"EXEC",111:"CHAR",112:"LOWER",113:"UPPER",114:"PROPER",115:"LEFT",116:"RIGHT",117:"EXACT",118:"TRIM",119:"REPLACE",120:"SUBSTITUTE",121:"CODE",122:"NAMES",123:"DIRECTORY",124:"FIND",125:"CELL",126:"ISERR",127:"ISTEXT",128:"ISNUMBER",129:"ISBLANK",130:"T",131:"N",132:"FOPEN",133:"FCLOSE",134:"FSIZE",135:"FREADLN",136:"FREAD",137:"FWRITELN",138:"FWRITE",139:"FPOS",140:"DATEVALUE",141:"TIMEVALUE",142:"SLN",143:"SYD",144:"DDB",145:"GET.DEF",146:"REFTEXT",147:"TEXTREF",148:"INDIRECT",149:"REGISTER",150:"CALL",151:"ADD.BAR",152:"ADD.MENU",153:"ADD.COMMAND",154:"ENABLE.COMMAND",155:"CHECK.COMMAND",156:"RENAME.COMMAND",157:"SHOW.BAR",158:"DELETE.MENU",159:"DELETE.COMMAND",160:"GET.CHART.ITEM",161:"DIALOG.BOX",162:"CLEAN",163:"MDETERM",164:"MINVERSE",165:"MMULT",166:"FILES",167:"IPMT",168:"PPMT",169:"COUNTA",170:"CANCEL.KEY",171:"FOR",172:"WHILE",173:"BREAK",174:"NEXT",175:"INITIATE",176:"REQUEST",177:"POKE",178:"EXECUTE",179:"TERMINATE",180:"RESTART",181:"HELP",182:"GET.BAR",183:"PRODUCT",184:"FACT",185:"GET.CELL",186:"GET.WORKSPACE",187:"GET.WINDOW",188:"GET.DOCUMENT",189:"DPRODUCT",190:"ISNONTEXT",191:"GET.NOTE",192:"NOTE",193:"STDEVP",194:"VARP",195:"DSTDEVP",196:"DVARP",197:"TRUNC",198:"ISLOGICAL",199:"DCOUNTA",200:"DELETE.BAR",201:"UNREGISTER",204:"USDOLLAR",205:"FINDB",206:"SEARCHB",207:"REPLACEB",208:"LEFTB",209:"RIGHTB",210:"MIDB",211:"LENB",212:"ROUNDUP",213:"ROUNDDOWN",214:"ASC",215:"DBCS",216:"RANK",219:"ADDRESS",220:"DAYS360",221:"TODAY",222:"VDB",223:"ELSE",224:"ELSE.IF",225:"END.IF",226:"FOR.CELL",227:"MEDIAN",228:"SUMPRODUCT",229:"SINH",230:"COSH",231:"TANH",232:"ASINH",233:"ACOSH",234:"ATANH",235:"DGET",236:"CREATE.OBJECT",237:"VOLATILE",238:"LAST.ERROR",239:"CUSTOM.UNDO",240:"CUSTOM.REPEAT",241:"FORMULA.CONVERT",242:"GET.LINK.INFO",243:"TEXT.BOX",244:"INFO",245:"GROUP",246:"GET.OBJECT",247:"DB",248:"PAUSE",251:"RESUME",252:"FREQUENCY",253:"ADD.TOOLBAR",254:"DELETE.TOOLBAR",255:"User",256:"RESET.TOOLBAR",257:"EVALUATE",258:"GET.TOOLBAR",259:"GET.TOOL",260:"SPELLING.CHECK",261:"ERROR.TYPE",262:"APP.TITLE",263:"WINDOW.TITLE",264:"SAVE.TOOLBAR",265:"ENABLE.TOOL",266:"PRESS.TOOL",267:"REGISTER.ID",268:"GET.WORKBOOK",269:"AVEDEV",270:"BETADIST",271:"GAMMALN",272:"BETAINV",273:"BINOMDIST",274:"CHIDIST",275:"CHIINV",276:"COMBIN",277:"CONFIDENCE",278:"CRITBINOM",279:"EVEN",280:"EXPONDIST",281:"FDIST",282:"FINV",283:"FISHER",284:"FISHERINV",285:"FLOOR",286:"GAMMADIST",287:"GAMMAINV",288:"CEILING",289:"HYPGEOMDIST",290:"LOGNORMDIST",291:"LOGINV",292:"NEGBINOMDIST",293:"NORMDIST",294:"NORMSDIST",295:"NORMINV",296:"NORMSINV",297:"STANDARDIZE",298:"ODD",299:"PERMUT",300:"POISSON",301:"TDIST",302:"WEIBULL",303:"SUMXMY2",304:"SUMX2MY2",305:"SUMX2PY2",306:"CHITEST",307:"CORREL",308:"COVAR",309:"FORECAST",310:"FTEST",311:"INTERCEPT",312:"PEARSON",313:"RSQ",314:"STEYX",315:"SLOPE",316:"TTEST",317:"PROB",318:"DEVSQ",319:"GEOMEAN",320:"HARMEAN",321:"SUMSQ",322:"KURT",323:"SKEW",324:"ZTEST",325:"LARGE",326:"SMALL",327:"QUARTILE",328:"PERCENTILE",329:"PERCENTRANK",330:"MODE",331:"TRIMMEAN",332:"TINV",334:"MOVIE.COMMAND",335:"GET.MOVIE",336:"CONCATENATE",337:"POWER",338:"PIVOT.ADD.DATA",339:"GET.PIVOT.TABLE",340:"GET.PIVOT.FIELD",341:"GET.PIVOT.ITEM",342:"RADIANS",343:"DEGREES",344:"SUBTOTAL",345:"SUMIF",346:"COUNTIF",347:"COUNTBLANK",348:"SCENARIO.GET",349:"OPTIONS.LISTS.GET",350:"ISPMT",351:"DATEDIF",352:"DATESTRING",353:"NUMBERSTRING",354:"ROMAN",355:"OPEN.DIALOG",356:"SAVE.DIALOG",357:"VIEW.GET",358:"GETPIVOTDATA",359:"HYPERLINK",360:"PHONETIC",361:"AVERAGEA",362:"MAXA",363:"MINA",364:"STDEVPA",365:"VARPA",366:"STDEVA",367:"VARA",368:"BAHTTEXT",369:"THAIDAYOFWEEK",370:"THAIDIGIT",371:"THAIMONTHOFYEAR",372:"THAINUMSOUND",373:"THAINUMSTRING",374:"THAISTRINGLENGTH",375:"ISTHAIDIGIT",376:"ROUNDBAHTDOWN",377:"ROUNDBAHTUP",378:"THAIYEAR",379:"RTD",380:"CUBEVALUE",381:"CUBEMEMBER",382:"CUBEMEMBERPROPERTY",383:"CUBERANKEDMEMBER",384:"HEX2BIN",385:"HEX2DEC",386:"HEX2OCT",387:"DEC2BIN",388:"DEC2HEX",389:"DEC2OCT",390:"OCT2BIN",391:"OCT2HEX",392:"OCT2DEC",393:"BIN2DEC",394:"BIN2OCT",395:"BIN2HEX",396:"IMSUB",397:"IMDIV",398:"IMPOWER",399:"IMABS",400:"IMSQRT",401:"IMLN",402:"IMLOG2",403:"IMLOG10",404:"IMSIN",405:"IMCOS",406:"IMEXP",407:"IMARGUMENT",408:"IMCONJUGATE",409:"IMAGINARY",410:"IMREAL",411:"COMPLEX",412:"IMSUM",413:"IMPRODUCT",414:"SERIESSUM",415:"FACTDOUBLE",416:"SQRTPI",417:"QUOTIENT",418:"DELTA",419:"GESTEP",420:"ISEVEN",421:"ISODD",422:"MROUND",423:"ERF",424:"ERFC",425:"BESSELJ",426:"BESSELK",427:"BESSELY",428:"BESSELI",429:"XIRR",430:"XNPV",431:"PRICEMAT",432:"YIELDMAT",433:"INTRATE",434:"RECEIVED",435:"DISC",436:"PRICEDISC",437:"YIELDDISC",438:"TBILLEQ",439:"TBILLPRICE",440:"TBILLYIELD",441:"PRICE",442:"YIELD",443:"DOLLARDE",444:"DOLLARFR",445:"NOMINAL",446:"EFFECT",447:"CUMPRINC",448:"CUMIPMT",449:"EDATE",450:"EOMONTH",451:"YEARFRAC",452:"COUPDAYBS",453:"COUPDAYS",454:"COUPDAYSNC",455:"COUPNCD",456:"COUPNUM",457:"COUPPCD",458:"DURATION",459:"MDURATION",460:"ODDLPRICE",461:"ODDLYIELD",462:"ODDFPRICE",463:"ODDFYIELD",464:"RANDBETWEEN",465:"WEEKNUM",466:"AMORDEGRC",467:"AMORLINC",468:"CONVERT",724:"SHEETJS",469:"ACCRINT",470:"ACCRINTM",471:"WORKDAY",472:"NETWORKDAYS",473:"GCD",474:"MULTINOMIAL",475:"LCM",476:"FVSCHEDULE",477:"CUBEKPIMEMBER",478:"CUBESET",479:"CUBESETCOUNT",480:"IFERROR",481:"COUNTIFS",482:"SUMIFS",483:"AVERAGEIF",484:"AVERAGEIFS"},fS={2:1,3:1,10:0,15:1,16:1,17:1,18:1,19:0,20:1,21:1,22:1,23:1,24:1,25:1,26:1,27:2,30:2,31:3,32:1,33:1,34:0,35:0,38:1,39:2,40:3,41:3,42:3,43:3,44:3,45:3,47:3,48:2,53:1,61:3,63:0,65:3,66:3,67:1,68:1,69:1,70:1,71:1,72:1,73:1,74:0,75:1,76:1,77:1,79:2,80:2,83:1,85:0,86:1,89:0,90:1,94:0,95:0,97:2,98:1,99:1,101:3,102:3,105:1,106:1,108:2,111:1,112:1,113:1,114:1,117:2,118:1,119:4,121:1,126:1,127:1,128:1,129:1,130:1,131:1,133:1,134:1,135:1,136:2,137:2,138:2,140:1,141:1,142:3,143:4,144:4,161:1,162:1,163:1,164:1,165:2,172:1,175:2,176:2,177:3,178:2,179:1,184:1,186:1,189:3,190:1,195:3,196:3,197:1,198:1,199:3,201:1,207:4,210:3,211:1,212:2,213:2,214:1,215:1,225:0,229:1,230:1,231:1,232:1,233:1,234:1,235:3,244:1,247:4,252:2,257:1,261:1,271:1,273:4,274:2,275:2,276:2,277:3,278:3,279:1,280:3,281:3,282:3,283:1,284:1,285:2,286:4,287:3,288:2,289:4,290:3,291:3,292:3,293:4,294:1,295:3,296:1,297:3,298:1,299:2,300:3,301:3,302:4,303:2,304:2,305:2,306:2,307:2,308:2,309:3,310:2,311:2,312:2,313:2,314:2,315:2,316:4,325:2,326:2,327:2,328:2,331:2,332:2,337:2,342:1,343:1,346:2,347:1,350:4,351:3,352:1,353:2,360:1,368:1,369:1,370:1,371:1,372:1,373:1,374:1,375:1,376:1,377:1,378:1,382:3,385:1,392:1,393:1,396:2,397:2,398:2,399:1,400:1,401:1,402:1,403:1,404:1,405:1,406:1,407:1,408:1,409:1,410:1,414:4,415:1,416:1,417:2,420:1,421:1,422:2,424:1,425:2,426:2,427:2,428:2,430:3,438:3,439:3,440:3,443:2,444:2,445:2,446:2,447:6,448:6,449:2,450:2,464:2,468:3,476:2,479:1,480:2,65535:0};function hS(e){var t="of:="+e.replace(Xl,"$1[.$2$3$4$5]").replace(/\]:\[/g,":");return t.replace(/;/g,"|").replace(/,/g,";")}function mS(e){return e.replace(/\./,"!")}var oa=typeof Map<"u";function Ql(e,t,r){var n=0,s=e.length;if(r){if(oa?r.has(t):Object.prototype.hasOwnProperty.call(r,t)){for(var i=oa?r.get(t):r[t];n-1?(r.width=ml(n),r.customWidth=1):t.width!=null&&(r.width=t.width),t.hidden&&(r.hidden=!0),t.level!=null&&(r.outlineLevel=r.level=t.level),r}function mh(e,t){if(e){var r=[.7,.7,.75,.75,.3,.3];e.left==null&&(e.left=r[0]),e.right==null&&(e.right=r[1]),e.top==null&&(e.top=r[2]),e.bottom==null&&(e.bottom=r[3]),e.header==null&&(e.header=r[4]),e.footer==null&&(e.footer=r[5])}}function Bn(e,t,r){var n=r.revssf[t.z!=null?t.z:"General"],s=60,i=e.length;if(n==null&&r.ssf){for(;s<392;++s)if(r.ssf[s]==null){cf(t.z,s),r.ssf[s]=t.z,r.revssf[t.z]=n=s;break}}for(s=0;s!=i;++s)if(e[s].numFmtId===n)return s;return e[i]={numFmtId:n,fontId:0,fillId:0,borderId:0,xfId:0,applyNumberFormat:1},i}function xS(e,t,r){if(e&&e["!ref"]){var n=vt(e["!ref"]);if(n.e.c',r=0;r!=e.length;++r)t+='';return t+""}function gS(e,t,r,n,s){var i=!1,o={},l=null;if(n.bookType!=="xlsx"&&t.vbaraw){var c=t.SheetNames[r];try{t.Workbook&&(c=t.Workbook.Sheets[r].CodeName||c)}catch{}i=!0,o.codeName=ya(ht(c))}if(e&&e["!outline"]){var d={summaryBelow:1,summaryRight:1};e["!outline"].above&&(d.summaryBelow=0),e["!outline"].left&&(d.summaryRight=0),l=(l||"")+Fe("outlinePr",null,d)}!i&&!l||(s[s.length]=Fe("sheetPr",l,o))}var _S=["objects","scenarios","selectLockedCells","selectUnlockedCells"],vS=["formatColumns","formatRows","formatCells","insertColumns","insertRows","insertHyperlinks","deleteColumns","deleteRows","sort","autoFilter","pivotTables"];function yS(e){var t={sheet:1};return _S.forEach(function(r){e[r]!=null&&e[r]&&(t[r]="1")}),vS.forEach(function(r){e[r]!=null&&!e[r]&&(t[r]="0")}),e.password&&(t.password=Jf(e.password).toString(16).toUpperCase()),Fe("sheetProtection",null,t)}function wS(e){return mh(e),Fe("pageMargins",null,e)}function SS(e,t){for(var r=[""],n,s=0;s!=t.length;++s)(n=t[s])&&(r[r.length]=Fe("col",null,xo(s,n)));return r[r.length]="",r.join("")}function TS(e,t,r,n){var s=typeof e.ref=="string"?e.ref:Ot(e.ref);r.Workbook||(r.Workbook={Sheets:[]}),r.Workbook.Names||(r.Workbook.Names=[]);var i=r.Workbook.Names,o=Ir(s);o.s.r==o.e.r&&(o.e.r=Ir(t["!ref"]).e.r,s=Ot(o));for(var l=0;l"u"&&(e.z=jt[14]);break;default:s=e.v;break}var l=qt("v",ht(s)),c={r:t},d=Bn(n.cellXfs,e,n);switch(d!==0&&(c.s=d),e.t){case"n":break;case"d":c.t="d";break;case"b":c.t="b";break;case"e":c.t="e";break;case"z":break;default:if(e.v==null){delete e.t;break}if(e.v.length>32767)throw new Error("Text length must not exceed 32767 characters");if(n&&n.bookSST){l=qt("v",""+Ql(n.Strings,e.v,n.revStrings)),c.t="s";break}c.t="str";break}if(e.t!=i&&(e.t=i,e.v=o),typeof e.f=="string"&&e.f){var u=e.F&&e.F.slice(0,t.length)==t?{t:"array",ref:e.F}:null;l=Fe("f",ht(e.f),u)+(e.v!=null?l:"")}return e.l&&r["!links"].push([t,e.l]),e.D&&(c.cm=1),Fe("c",l,c)}function jS(e,t,r,n){var s=[],i=[],o=vt(e["!ref"]),l="",c,d="",u=[],h=0,m=0,_=e["!rows"],x=Array.isArray(e),f={r:d},p,I=-1;for(m=o.s.c;m<=o.e.c;++m)u[m]=sr(m);for(h=o.s.r;h<=o.e.r;++h){for(i=[],d=Jt(h),m=o.s.c;m<=o.e.c;++m){c=u[m]+d;var S=x?(e[h]||[])[m]:e[c];S!==void 0&&(l=ES(S,c,e,t))!=null&&i.push(l)}(i.length>0||_&&_[h])&&(f={r:d},_&&_[h]&&(p=_[h],p.hidden&&(f.hidden=1),I=-1,p.hpx?I=Mi(p.hpx):p.hpt&&(I=p.hpt),I>-1&&(f.ht=I,f.customHeight=1),p.level&&(f.outlineLevel=p.level)),s[s.length]=Fe("row",i.join(""),f))}if(_)for(;h<_.length;++h)_&&_[h]&&(f={r:h+1},p=_[h],p.hidden&&(f.hidden=1),I=-1,p.hpx?I=Mi(p.hpx):p.hpt&&(I=p.hpt),I>-1&&(f.ht=I,f.customHeight=1),p.level&&(f.outlineLevel=p.level),s[s.length]=Fe("row","",f));return s.join("")}function xh(e,t,r,n){var s=[Rt,Fe("worksheet",null,{xmlns:Ns[0],"xmlns:r":Ut.r})],i=r.SheetNames[e],o=0,l="",c=r.Sheets[i];c==null&&(c={});var d=c["!ref"]||"A1",u=vt(d);if(u.e.c>16383||u.e.r>1048575){if(t.WTF)throw new Error("Range "+d+" exceeds format limit A1:XFD1048576");u.e.c=Math.min(u.e.c,16383),u.e.r=Math.min(u.e.c,1048575),d=Ot(u)}n||(n={}),c["!comments"]=[];var h=[];gS(c,r,e,t,s),s[s.length]=Fe("dimension",null,{ref:d}),s[s.length]=bS(c,t,e,r),t.sheetFormat&&(s[s.length]=Fe("sheetFormatPr",null,{defaultRowHeight:t.sheetFormat.defaultRowHeight||"16",baseColWidth:t.sheetFormat.baseColWidth||"10",outlineLevelRow:t.sheetFormat.outlineLevelRow||"7"})),c["!cols"]!=null&&c["!cols"].length>0&&(s[s.length]=SS(c,c["!cols"])),s[o=s.length]="",c["!links"]=[],c["!ref"]!=null&&(l=jS(c,t),l.length>0&&(s[s.length]=l)),s.length>o+1&&(s[s.length]="",s[o]=s[o].replace("/>",">")),c["!protect"]&&(s[s.length]=yS(c["!protect"])),c["!autofilter"]!=null&&(s[s.length]=TS(c["!autofilter"],c,r,e)),c["!merges"]!=null&&c["!merges"].length>0&&(s[s.length]=pS(c["!merges"]));var m=-1,_,x=-1;return c["!links"].length>0&&(s[s.length]="",c["!links"].forEach(function(f){f[1].Target&&(_={ref:f[0]},f[1].Target.charAt(0)!="#"&&(x=ft(n,-1,ht(f[1].Target).replace(/#.*$/,""),it.HLINK),_["r:id"]="rId"+x),(m=f[1].Target.indexOf("#"))>-1&&(_.location=ht(f[1].Target.slice(m+1))),f[1].Tooltip&&(_.tooltip=ht(f[1].Tooltip)),s[s.length]=Fe("hyperlink",null,_))}),s[s.length]=""),delete c["!links"],c["!margins"]!=null&&(s[s.length]=wS(c["!margins"])),(!t||t.ignoreEC||t.ignoreEC==null)&&(s[s.length]=qt("ignoredErrors",Fe("ignoredError",null,{numberStoredAsText:1,sqref:d}))),h.length>0&&(x=ft(n,-1,"../drawings/drawing"+(e+1)+".xml",it.DRAW),s[s.length]=Fe("drawing",null,{"r:id":"rId"+x}),c["!drawing"]=h),c["!comments"].length>0&&(x=ft(n,-1,"../drawings/vmlDrawing"+(e+1)+".vml",it.VML),s[s.length]=Fe("legacyDrawing",null,{"r:id":"rId"+x}),c["!legacy"]=x),s.length>1&&(s[s.length]="",s[1]=s[1].replace("/>",">")),s.join("")}function IS(e,t){var r={},n=e.l+t;r.r=e.read_shift(4),e.l+=4;var s=e.read_shift(2);e.l+=1;var i=e.read_shift(1);return e.l=n,i&7&&(r.level=i&7),i&16&&(r.hidden=!0),i&32&&(r.hpt=s/20),r}function CS(e,t,r){var n=pe(145),s=(r["!rows"]||[])[e]||{};n.write_shift(4,e),n.write_shift(4,0);var i=320;s.hpx?i=Mi(s.hpx)*20:s.hpt&&(i=s.hpt*20),n.write_shift(2,i),n.write_shift(1,0);var o=0;s.level&&(o|=s.level),s.hidden&&(o|=16),(s.hpx||s.hpt)&&(o|=32),n.write_shift(1,o),n.write_shift(1,0);var l=0,c=n.l;n.l+=4;for(var d={r:e,c:0},u=0;u<16;++u)if(!(t.s.c>u+1<<10||t.e.cn.l?n.slice(0,n.l):n}function AS(e,t,r,n){var s=CS(n,r,t);(s.length>17||(t["!rows"]||[])[n])&&Se(e,0,s)}var kS=ss,FS=Ms;function OS(){}function RS(e,t){var r={},n=e[e.l];return++e.l,r.above=!(n&64),r.left=!(n&128),e.l+=18,r.name=Wv(e),r}function DS(e,t,r){r==null&&(r=pe(84+4*e.length));var n=192;t&&(t.above&&(n&=-65),t.left&&(n&=-129)),r.write_shift(1,n);for(var s=1;s<3;++s)r.write_shift(1,0);return Di({auto:1},r),r.write_shift(-4,-1),r.write_shift(-4,-1),kf(e,r),r.slice(0,r.l)}function PS(e){var t=Br(e);return[t]}function NS(e,t,r){return r==null&&(r=pe(8)),ts(t,r)}function LS(e){var t=rs(e);return[t]}function MS(e,t,r){return r==null&&(r=pe(4)),ns(t,r)}function BS(e){var t=Br(e),r=e.read_shift(1);return[t,r,"b"]}function US(e,t,r){return r==null&&(r=pe(9)),ts(t,r),r.write_shift(1,e.v?1:0),r}function zS(e){var t=rs(e),r=e.read_shift(1);return[t,r,"b"]}function $S(e,t,r){return r==null&&(r=pe(5)),ns(t,r),r.write_shift(1,e.v?1:0),r}function WS(e){var t=Br(e),r=e.read_shift(1);return[t,r,"e"]}function HS(e,t,r){return r==null&&(r=pe(9)),ts(t,r),r.write_shift(1,e.v),r}function VS(e){var t=rs(e),r=e.read_shift(1);return[t,r,"e"]}function KS(e,t,r){return r==null&&(r=pe(8)),ns(t,r),r.write_shift(1,e.v),r.write_shift(2,0),r.write_shift(1,0),r}function GS(e){var t=Br(e),r=e.read_shift(4);return[t,r,"s"]}function YS(e,t,r){return r==null&&(r=pe(12)),ts(t,r),r.write_shift(4,t.v),r}function qS(e){var t=rs(e),r=e.read_shift(4);return[t,r,"s"]}function XS(e,t,r){return r==null&&(r=pe(8)),ns(t,r),r.write_shift(4,t.v),r}function JS(e){var t=Br(e),r=Bs(e);return[t,r,"n"]}function ZS(e,t,r){return r==null&&(r=pe(16)),ts(t,r),Zn(e.v,r),r}function QS(e){var t=rs(e),r=Bs(e);return[t,r,"n"]}function eT(e,t,r){return r==null&&(r=pe(12)),ns(t,r),Zn(e.v,r),r}function tT(e){var t=Br(e),r=Ff(e);return[t,r,"n"]}function rT(e,t,r){return r==null&&(r=pe(12)),ts(t,r),Of(e.v,r),r}function nT(e){var t=rs(e),r=Ff(e);return[t,r,"n"]}function sT(e,t,r){return r==null&&(r=pe(8)),ns(t,r),Of(e.v,r),r}function aT(e){var t=Br(e),r=Vl(e);return[t,r,"is"]}function iT(e){var t=Br(e),r=ar(e);return[t,r,"str"]}function oT(e,t,r){return r==null&&(r=pe(12+4*e.v.length)),ts(t,r),$t(e.v,r),r.length>r.l?r.slice(0,r.l):r}function lT(e){var t=rs(e),r=ar(e);return[t,r,"str"]}function cT(e,t,r){return r==null&&(r=pe(8+4*e.v.length)),ns(t,r),$t(e.v,r),r.length>r.l?r.slice(0,r.l):r}function dT(e,t,r){var n=e.l+t,s=Br(e);s.r=r["!row"];var i=e.read_shift(1),o=[s,i,"b"];if(r.cellFormula){e.l+=2;var l=mo(e,n-e.l,r);o[3]=As(l,null,s,r.supbooks,r)}else e.l=n;return o}function uT(e,t,r){var n=e.l+t,s=Br(e);s.r=r["!row"];var i=e.read_shift(1),o=[s,i,"e"];if(r.cellFormula){e.l+=2;var l=mo(e,n-e.l,r);o[3]=As(l,null,s,r.supbooks,r)}else e.l=n;return o}function fT(e,t,r){var n=e.l+t,s=Br(e);s.r=r["!row"];var i=Bs(e),o=[s,i,"n"];if(r.cellFormula){e.l+=2;var l=mo(e,n-e.l,r);o[3]=As(l,null,s,r.supbooks,r)}else e.l=n;return o}function hT(e,t,r){var n=e.l+t,s=Br(e);s.r=r["!row"];var i=ar(e),o=[s,i,"str"];if(r.cellFormula){e.l+=2;var l=mo(e,n-e.l,r);o[3]=As(l,null,s,r.supbooks,r)}else e.l=n;return o}var mT=ss,xT=Ms;function pT(e,t){return t==null&&(t=pe(4)),t.write_shift(4,e),t}function gT(e,t){var r=e.l+t,n=ss(e),s=Kl(e),i=ar(e),o=ar(e),l=ar(e);e.l=r;var c={rfx:n,relId:s,loc:i,display:l};return o&&(c.Tooltip=o),c}function _T(e,t){var r=pe(50+4*(e[1].Target.length+(e[1].Tooltip||"").length));Ms({s:zt(e[0]),e:zt(e[0])},r),Gl("rId"+t,r);var n=e[1].Target.indexOf("#"),s=n==-1?"":e[1].Target.slice(n+1);return $t(s||"",r),$t(e[1].Tooltip||"",r),$t("",r),r.slice(0,r.l)}function vT(){}function yT(e,t,r){var n=e.l+t,s=Rf(e),i=e.read_shift(1),o=[s];if(o[2]=i,r.cellFormula){var l=lS(e,n-e.l,r);o[1]=l}else e.l=n;return o}function wT(e,t,r){var n=e.l+t,s=ss(e),i=[s];if(r.cellFormula){var o=dS(e,n-e.l,r);i[1]=o,e.l=n}else e.l=n;return i}function ST(e,t,r){r==null&&(r=pe(18));var n=xo(e,t);r.write_shift(-4,e),r.write_shift(-4,e),r.write_shift(4,(n.width||10)*256),r.write_shift(4,0);var s=0;return t.hidden&&(s|=1),typeof n.width=="number"&&(s|=2),t.level&&(s|=t.level<<8),r.write_shift(2,s),r}var ph=["left","right","top","bottom","header","footer"];function TT(e){var t={};return ph.forEach(function(r){t[r]=Bs(e)}),t}function bT(e,t){return t==null&&(t=pe(6*8)),mh(e),ph.forEach(function(r){Zn(e[r],t)}),t}function ET(e){var t=e.read_shift(2);return e.l+=28,{RTL:t&32}}function jT(e,t,r){r==null&&(r=pe(30));var n=924;return(((t||{}).Views||[])[0]||{}).RTL&&(n|=32),r.write_shift(2,n),r.write_shift(4,0),r.write_shift(4,0),r.write_shift(4,0),r.write_shift(1,0),r.write_shift(1,0),r.write_shift(2,0),r.write_shift(2,100),r.write_shift(2,0),r.write_shift(2,0),r.write_shift(2,0),r.write_shift(4,0),r}function IT(e){var t=pe(24);return t.write_shift(4,4),t.write_shift(4,1),Ms(e,t),t}function CT(e,t){return t==null&&(t=pe(16*4+2)),t.write_shift(2,e.password?Jf(e.password):0),t.write_shift(4,1),[["objects",!1],["scenarios",!1],["formatCells",!0],["formatColumns",!0],["formatRows",!0],["insertColumns",!0],["insertRows",!0],["insertHyperlinks",!0],["deleteColumns",!0],["deleteRows",!0],["selectLockedCells",!1],["sort",!0],["autoFilter",!0],["pivotTables",!0],["selectUnlockedCells",!1]].forEach(function(r){r[1]?t.write_shift(4,e[r[0]]!=null&&!e[r[0]]?1:0):t.write_shift(4,e[r[0]]!=null&&e[r[0]]?0:1)}),t}function AT(){}function kT(){}function FT(e,t,r,n,s,i,o){if(t.v===void 0)return!1;var l="";switch(t.t){case"b":l=t.v?"1":"0";break;case"d":t=gr(t),t.z=t.z||jt[14],t.v=pr(lr(t.v)),t.t="n";break;case"n":case"e":l=""+t.v;break;default:l=t.v;break}var c={r,c:n};switch(c.s=Bn(s.cellXfs,t,s),t.l&&i["!links"].push([mt(c),t.l]),t.c&&i["!comments"].push([mt(c),t.c]),t.t){case"s":case"str":return s.bookSST?(l=Ql(s.Strings,t.v,s.revStrings),c.t="s",c.v=l,o?Se(e,18,XS(t,c)):Se(e,7,YS(t,c))):(c.t="str",o?Se(e,17,cT(t,c)):Se(e,6,oT(t,c))),!0;case"n":return t.v==(t.v|0)&&t.v>-1e3&&t.v<1e3?o?Se(e,13,sT(t,c)):Se(e,2,rT(t,c)):o?Se(e,16,eT(t,c)):Se(e,5,ZS(t,c)),!0;case"b":return c.t="b",o?Se(e,15,$S(t,c)):Se(e,4,US(t,c)),!0;case"e":return c.t="e",o?Se(e,14,KS(t,c)):Se(e,3,HS(t,c)),!0}return o?Se(e,12,MS(t,c)):Se(e,1,NS(t,c)),!0}function OT(e,t,r,n){var s=vt(t["!ref"]||"A1"),i,o="",l=[];Se(e,145);var c=Array.isArray(t),d=s.e.r;t["!rows"]&&(d=Math.max(s.e.r,t["!rows"].length-1));for(var u=s.s.r;u<=d;++u){o=Jt(u),AS(e,t,s,u);var h=!1;if(u<=s.e.r)for(var m=s.s.c;m<=s.e.c;++m){u===s.s.r&&(l[m]=sr(m)),i=l[m]+o;var _=c?(t[u]||[])[m]:t[i];if(!_){h=!1;continue}h=FT(e,_,u,m,n,t,h)}}Se(e,146)}function RT(e,t){!t||!t["!merges"]||(Se(e,177,pT(t["!merges"].length)),t["!merges"].forEach(function(r){Se(e,176,xT(r))}),Se(e,178))}function DT(e,t){!t||!t["!cols"]||(Se(e,390),t["!cols"].forEach(function(r,n){r&&Se(e,60,ST(n,r))}),Se(e,391))}function PT(e,t){!t||!t["!ref"]||(Se(e,648),Se(e,649,IT(vt(t["!ref"]))),Se(e,650))}function NT(e,t,r){t["!links"].forEach(function(n){if(n[1].Target){var s=ft(r,-1,n[1].Target.replace(/#.*$/,""),it.HLINK);Se(e,494,_T(n,s))}}),delete t["!links"]}function LT(e,t,r,n){if(t["!comments"].length>0){var s=ft(n,-1,"../drawings/vmlDrawing"+(r+1)+".vml",it.VML);Se(e,551,Gl("rId"+s)),t["!legacy"]=s}}function MT(e,t,r,n){if(t["!autofilter"]){var s=t["!autofilter"],i=typeof s.ref=="string"?s.ref:Ot(s.ref);r.Workbook||(r.Workbook={Sheets:[]}),r.Workbook.Names||(r.Workbook.Names=[]);var o=r.Workbook.Names,l=Ir(i);l.s.r==l.e.r&&(l.e.r=Ir(t["!ref"]).e.r,i=Ot(l));for(var c=0;c16383||c.e.r>1048575){if(t.WTF)throw new Error("Range "+(o["!ref"]||"A1")+" exceeds format limit A1:XFD1048576");c.e.c=Math.min(c.e.c,16383),c.e.r=Math.min(c.e.c,1048575)}return o["!links"]=[],o["!comments"]=[],Se(s,129),(r.vbaraw||o["!outline"])&&Se(s,147,DS(l,o["!outline"])),Se(s,148,FS(c)),BT(s,o,r.Workbook),DT(s,o),OT(s,o,e,t),UT(s,o),MT(s,o,r,e),RT(s,o),NT(s,o,n),o["!margins"]&&Se(s,476,bT(o["!margins"])),(!t||t.ignoreEC||t.ignoreEC==null)&&PT(s,o),LT(s,o,e,n),Se(s,130),s.end()}function $T(e,t){e.l+=10;var r=ar(e);return{name:r}}var WT=[["allowRefreshQuery",!1,"bool"],["autoCompressPictures",!0,"bool"],["backupFile",!1,"bool"],["checkCompatibility",!1,"bool"],["CodeName",""],["date1904",!1,"bool"],["defaultThemeVersion",0,"int"],["filterPrivacy",!1,"bool"],["hidePivotFieldList",!1,"bool"],["promptedSolutions",!1,"bool"],["publishItems",!1,"bool"],["refreshAllConnections",!1,"bool"],["saveExternalLinkValues",!0,"bool"],["showBorderUnselectedTables",!0,"bool"],["showInkAnnotation",!0,"bool"],["showObjects","all"],["showPivotChartFilter",!1,"bool"],["updateLinks","userSet"]];function HT(e){return!e.Workbook||!e.Workbook.WBProps?"false":wv(e.Workbook.WBProps.date1904)?"true":"false"}var VT="][*?/\\".split("");function gh(e,t){if(e.length>31)throw new Error("Sheet names cannot exceed 31 chars");var r=!0;return VT.forEach(function(n){if(e.indexOf(n)!=-1)throw new Error("Sheet name cannot contain : \\ / ? * [ ]")}),r}function KT(e,t,r){e.forEach(function(n,s){gh(n);for(var i=0;i22)throw new Error("Bad Code Name: Worksheet"+o)}})}function GT(e){if(!e||!e.SheetNames||!e.Sheets)throw new Error("Invalid Workbook");if(!e.SheetNames.length)throw new Error("Workbook is empty");var t=e.Workbook&&e.Workbook.Sheets||[];KT(e.SheetNames,t,!!e.vbaraw);for(var r=0;r0,n={codeName:"ThisWorkbook"};e.Workbook&&e.Workbook.WBProps&&(WT.forEach(function(l){e.Workbook.WBProps[l[0]]!=null&&e.Workbook.WBProps[l[0]]!=l[1]&&(n[l[0]]=e.Workbook.WBProps[l[0]])}),e.Workbook.WBProps.CodeName&&(n.codeName=e.Workbook.WBProps.CodeName,delete n.CodeName)),t[t.length]=Fe("workbookPr",null,n);var s=e.Workbook&&e.Workbook.Sheets||[],i=0;if(s&&s[0]&&s[0].Hidden){for(t[t.length]="",i=0;i!=e.SheetNames.length&&!(!s[i]||!s[i].Hidden);++i);i==e.SheetNames.length&&(i=0),t[t.length]='',t[t.length]=""}for(t[t.length]="",i=0;i!=e.SheetNames.length;++i){var o={name:ht(e.SheetNames[i].slice(0,31))};if(o.sheetId=""+(i+1),o["r:id"]="rId"+(i+1),s[i])switch(s[i].Hidden){case 1:o.state="hidden";break;case 2:o.state="veryHidden";break}t[t.length]=Fe("sheet",null,o)}return t[t.length]="",r&&(t[t.length]="",e.Workbook&&e.Workbook.Names&&e.Workbook.Names.forEach(function(l){var c={name:l.Name};l.Comment&&(c.comment=l.Comment),l.Sheet!=null&&(c.localSheetId=""+l.Sheet),l.Hidden&&(c.hidden="1"),l.Ref&&(t[t.length]=Fe("definedName",ht(l.Ref),c))}),t[t.length]=""),t.length>2&&(t[t.length]="",t[1]=t[1].replace("/>",">")),t.join("")}function YT(e,t){var r={};return r.Hidden=e.read_shift(4),r.iTabID=e.read_shift(4),r.strRelID=hl(e),r.name=ar(e),r}function qT(e,t){return t||(t=pe(127)),t.write_shift(4,e.Hidden),t.write_shift(4,e.iTabID),Gl(e.strRelID,t),$t(e.name.slice(0,31),t),t.length>t.l?t.slice(0,t.l):t}function XT(e,t){var r={},n=e.read_shift(4);r.defaultThemeVersion=e.read_shift(4);var s=t>8?ar(e):"";return s.length>0&&(r.CodeName=s),r.autoCompressPictures=!!(n&65536),r.backupFile=!!(n&64),r.checkCompatibility=!!(n&4096),r.date1904=!!(n&1),r.filterPrivacy=!!(n&8),r.hidePivotFieldList=!!(n&1024),r.promptedSolutions=!!(n&16),r.publishItems=!!(n&2048),r.refreshAllConnections=!!(n&262144),r.saveExternalLinkValues=!!(n&128),r.showBorderUnselectedTables=!!(n&4),r.showInkAnnotation=!!(n&32),r.showObjects=["all","placeholders","none"][n>>13&3],r.showPivotChartFilter=!!(n&32768),r.updateLinks=["userSet","never","always"][n>>8&3],r}function JT(e,t){t||(t=pe(72));var r=0;return e&&e.filterPrivacy&&(r|=8),t.write_shift(4,r),t.write_shift(4,0),kf(e&&e.CodeName||"ThisWorkbook",t),t.slice(0,t.l)}function ZT(e,t,r){var n=e.l+t;e.l+=4,e.l+=1;var s=e.read_shift(4),i=Hv(e),o=cS(e,0,r),l=Kl(e);e.l=n;var c={Name:i,Ptg:o};return s<268435455&&(c.Sheet=s),l&&(c.Comment=l),c}function QT(e,t){Se(e,143);for(var r=0;r!=t.SheetNames.length;++r){var n=t.Workbook&&t.Workbook.Sheets&&t.Workbook.Sheets[r]&&t.Workbook.Sheets[r].Hidden||0,s={Hidden:n,iTabID:r+1,strRelID:"rId"+(r+1),name:t.SheetNames[r]};Se(e,156,qT(s))}Se(e,144)}function eb(e,t){t||(t=pe(127));for(var r=0;r!=4;++r)t.write_shift(4,0);return $t("SheetJS",t),$t(ji.version,t),$t(ji.version,t),$t("7262",t),t.length>t.l?t.slice(0,t.l):t}function tb(e,t){t||(t=pe(29)),t.write_shift(-4,0),t.write_shift(-4,460),t.write_shift(4,28800),t.write_shift(4,17600),t.write_shift(4,500),t.write_shift(4,e),t.write_shift(4,e);var r=120;return t.write_shift(1,r),t.length>t.l?t.slice(0,t.l):t}function rb(e,t){if(!(!t.Workbook||!t.Workbook.Sheets)){for(var r=t.Workbook.Sheets,n=0,s=-1,i=-1;ns||(Se(e,135),Se(e,158,tb(s)),Se(e,136))}}function nb(e,t){var r=xr();return Se(r,131),Se(r,128,eb()),Se(r,153,JT(e.Workbook&&e.Workbook.WBProps||null)),rb(r,e),QT(r,e),Se(r,132),r.end()}function sb(e,t,r){return(t.slice(-4)===".bin"?nb:_h)(e)}function ab(e,t,r,n,s){return(t.slice(-4)===".bin"?zT:xh)(e,r,n,s)}function ib(e,t,r){return(t.slice(-4)===".bin"?bw:eh)(e,r)}function ob(e,t,r){return(t.slice(-4)===".bin"?Jy:Xf)(e,r)}function lb(e,t,r){return(t.slice(-4)===".bin"?Uw:ah)(e)}function cb(e){return(e.slice(-4)===".bin"?Ow:nh)()}function db(e,t){var r=[];return e.Props&&r.push(iy(e.Props,t)),e.Custprops&&r.push(oy(e.Props,e.Custprops)),r.join("")}function ub(){return""}function fb(e,t){var r=[''];return t.cellXfs.forEach(function(n,s){var i=[];i.push(Fe("NumberFormat",null,{"ss:Format":ht(jt[n.numFmtId])}));var o={"ss:ID":"s"+(21+s)};r.push(Fe("Style",i.join(""),o))}),Fe("Styles",r.join(""))}function vh(e){return Fe("NamedRange",null,{"ss:Name":e.Name,"ss:RefersTo":"="+Jl(e.Ref,{r:0,c:0})})}function hb(e){if(!((e||{}).Workbook||{}).Names)return"";for(var t=e.Workbook.Names,r=[],n=0;n"),e["!margins"].header&&s.push(Fe("Header",null,{"x:Margin":e["!margins"].header})),e["!margins"].footer&&s.push(Fe("Footer",null,{"x:Margin":e["!margins"].footer})),s.push(Fe("PageMargins",null,{"x:Bottom":e["!margins"].bottom||"0.75","x:Left":e["!margins"].left||"0.7","x:Right":e["!margins"].right||"0.7","x:Top":e["!margins"].top||"0.75"})),s.push("")),n&&n.Workbook&&n.Workbook.Sheets&&n.Workbook.Sheets[r])if(n.Workbook.Sheets[r].Hidden)s.push(Fe("Visible",n.Workbook.Sheets[r].Hidden==1?"SheetHidden":"SheetVeryHidden",{}));else{for(var i=0;i")}return((((n||{}).Workbook||{}).Views||[])[0]||{}).RTL&&s.push(""),e["!protect"]&&(s.push(qt("ProtectContents","True")),e["!protect"].objects&&s.push(qt("ProtectObjects","True")),e["!protect"].scenarios&&s.push(qt("ProtectScenarios","True")),e["!protect"].selectLockedCells!=null&&!e["!protect"].selectLockedCells?s.push(qt("EnableSelection","NoSelection")):e["!protect"].selectUnlockedCells!=null&&!e["!protect"].selectUnlockedCells&&s.push(qt("EnableSelection","UnlockedCells")),[["formatCells","AllowFormatCells"],["formatColumns","AllowSizeCols"],["formatRows","AllowSizeRows"],["insertColumns","AllowInsertCols"],["insertRows","AllowInsertRows"],["insertHyperlinks","AllowInsertHyperlinks"],["deleteColumns","AllowDeleteCols"],["deleteRows","AllowDeleteRows"],["sort","AllowSort"],["autoFilter","AllowFilter"],["pivotTables","AllowUsePivotTables"]].forEach(function(o){e["!protect"][o[0]]&&s.push("<"+o[1]+"/>")})),s.length==0?"":Fe("WorksheetOptions",s.join(""),{xmlns:br.x})}function pb(e){return e.map(function(t){var r=yv(t.t||""),n=Fe("ss:Data",r,{xmlns:"http://www.w3.org/TR/REC-html40"});return Fe("Comment",n,{"ss:Author":t.a})}).join("")}function gb(e,t,r,n,s,i,o){if(!e||e.v==null&&e.f==null)return"";var l={};if(e.f&&(l["ss:Formula"]="="+ht(Jl(e.f,o))),e.F&&e.F.slice(0,t.length)==t){var c=zt(e.F.slice(t.length+1));l["ss:ArrayRange"]="RC:R"+(c.r==o.r?"":"["+(c.r-o.r)+"]")+"C"+(c.c==o.c?"":"["+(c.c-o.c)+"]")}if(e.l&&e.l.Target&&(l["ss:HRef"]=ht(e.l.Target),e.l.Tooltip&&(l["x:HRefScreenTip"]=ht(e.l.Tooltip))),r["!merges"])for(var d=r["!merges"],u=0;u!=d.length;++u)d[u].s.c!=o.c||d[u].s.r!=o.r||(d[u].e.c>d[u].s.c&&(l["ss:MergeAcross"]=d[u].e.c-d[u].s.c),d[u].e.r>d[u].s.r&&(l["ss:MergeDown"]=d[u].e.r-d[u].s.r));var h="",m="";switch(e.t){case"z":if(!n.sheetStubs)return"";break;case"n":h="Number",m=String(e.v);break;case"b":h="Boolean",m=e.v?"1":"0";break;case"e":h="Error",m=La[e.v];break;case"d":h="DateTime",m=new Date(e.v).toISOString(),e.z==null&&(e.z=e.z||jt[14]);break;case"s":h="String",m=vv(e.v||"");break}var _=Bn(n.cellXfs,e,n);l["ss:StyleID"]="s"+(21+_),l["ss:Index"]=o.c+1;var x=e.v!=null?m:"",f=e.t=="z"?"":''+x+"";return(e.c||[]).length>0&&(f+=pb(e.c)),Fe("Cell",f,l)}function _b(e,t){var r='"}function vb(e,t,r,n){if(!e["!ref"])return"";var s=vt(e["!ref"]),i=e["!merges"]||[],o=0,l=[];e["!cols"]&&e["!cols"].forEach(function(p,I){ql(p);var S=!!p.width,w=xo(I,p),v={"ss:Index":I+1};S&&(v["ss:Width"]=Ni(w.width)),p.hidden&&(v["ss:Hidden"]="1"),l.push(Fe("Column",null,v))});for(var c=Array.isArray(e),d=s.s.r;d<=s.e.r;++d){for(var u=[_b(d,(e["!rows"]||[])[d])],h=s.s.c;h<=s.e.c;++h){var m=!1;for(o=0;o!=i.length;++o)if(!(i[o].s.c>h)&&!(i[o].s.r>d)&&!(i[o].e.c"),u.length>2&&l.push(u.join(""))}return l.join("")}function yb(e,t,r){var n=[],s=r.SheetNames[e],i=r.Sheets[s],o=i?mb(i,t,e,r):"";return o.length>0&&n.push(""+o+""),o=i?vb(i,t,e,r):"",o.length>0&&n.push(""+o+"
"),n.push(xb(i,t,e,r)),n.join("")}function wb(e,t){t||(t={}),e.SSF||(e.SSF=gr(jt)),e.SSF&&(co(),lo(e.SSF),t.revssf=uo(e.SSF),t.revssf[e.SSF[65535]]=0,t.ssf=e.SSF,t.cellXfs=[],Bn(t.cellXfs,{},{revssf:{General:0}}));var r=[];r.push(db(e,t)),r.push(ub()),r.push(""),r.push("");for(var n=0;n-1||Uf.indexOf(s[i][0])>-1||s[i][1]!=null&&d.push(s[i]);n.length&&xt.utils.cfb_add(t,"/SummaryInformation",Ld(n,Bo.SI,c,Od)),(r.length||d.length)&&xt.utils.cfb_add(t,"/DocumentSummaryInformation",Ld(r,Bo.DSI,l,Fd,d.length?d:null,Bo.UDI))}function Tb(e,t){var r=t||{},n=xt.utils.cfb_new({root:"R"}),s="/Workbook";switch(r.bookType||"xls"){case"xls":r.bookType="biff8";case"xla":r.bookType||(r.bookType="xla");case"biff8":s="/Workbook",r.biff=8;break;case"biff5":s="/Book",r.biff=5;break;default:throw new Error("invalid type "+r.bookType+" for XLS CFB")}return xt.utils.cfb_add(n,s,yh(e,r)),r.biff==8&&(e.Props||e.Custprops)&&Sb(e,n),r.biff==8&&e.vbaraw&&zw(n,xt.read(e.vbaraw,{type:typeof e.vbaraw=="string"?"binary":"buffer"})),n}var bb={0:{f:IS},1:{f:PS},2:{f:tT},3:{f:WS},4:{f:BS},5:{f:JS},6:{f:iT},7:{f:GS},8:{f:hT},9:{f:fT},10:{f:dT},11:{f:uT},12:{f:LS},13:{f:nT},14:{f:VS},15:{f:zS},16:{f:QS},17:{f:lT},18:{f:qS},19:{f:Vl},20:{},21:{},22:{},23:{},24:{},25:{},26:{},27:{},28:{},29:{},30:{},31:{},32:{},33:{},34:{},35:{T:1},36:{T:-1},37:{T:1},38:{T:-1},39:{f:ZT},40:{},42:{},43:{f:iw},44:{f:sw},45:{f:cw},46:{f:uw},47:{f:dw},48:{},49:{f:Lv},50:{},51:{f:Iw},52:{T:1},53:{T:-1},54:{T:1},55:{T:-1},56:{T:1},57:{T:-1},58:{},59:{},60:{f:My},62:{f:aT},63:{f:Rw},64:{f:AT},65:{},66:{},67:{},68:{},69:{},70:{},128:{},129:{T:1},130:{T:-1},131:{T:1,f:an,p:0},132:{T:-1},133:{T:1},134:{T:-1},135:{T:1},136:{T:-1},137:{T:1,f:ET},138:{T:-1},139:{T:1},140:{T:-1},141:{T:1},142:{T:-1},143:{T:1},144:{T:-1},145:{T:1},146:{T:-1},147:{f:RS},148:{f:kS,p:16},151:{f:vT},152:{},153:{f:XT},154:{},155:{},156:{f:YT},157:{},158:{},159:{T:1,f:Yy},160:{T:-1},161:{T:1,f:ss},162:{T:-1},163:{T:1},164:{T:-1},165:{T:1},166:{T:-1},167:{},168:{},169:{},170:{},171:{},172:{T:1},173:{T:-1},174:{},175:{},176:{f:mT},177:{T:1},178:{T:-1},179:{T:1},180:{T:-1},181:{T:1},182:{T:-1},183:{T:1},184:{T:-1},185:{T:1},186:{T:-1},187:{T:1},188:{T:-1},189:{T:1},190:{T:-1},191:{T:1},192:{T:-1},193:{T:1},194:{T:-1},195:{T:1},196:{T:-1},197:{T:1},198:{T:-1},199:{T:1},200:{T:-1},201:{T:1},202:{T:-1},203:{T:1},204:{T:-1},205:{T:1},206:{T:-1},207:{T:1},208:{T:-1},209:{T:1},210:{T:-1},211:{T:1},212:{T:-1},213:{T:1},214:{T:-1},215:{T:1},216:{T:-1},217:{T:1},218:{T:-1},219:{T:1},220:{T:-1},221:{T:1},222:{T:-1},223:{T:1},224:{T:-1},225:{T:1},226:{T:-1},227:{T:1},228:{T:-1},229:{T:1},230:{T:-1},231:{T:1},232:{T:-1},233:{T:1},234:{T:-1},235:{T:1},236:{T:-1},237:{T:1},238:{T:-1},239:{T:1},240:{T:-1},241:{T:1},242:{T:-1},243:{T:1},244:{T:-1},245:{T:1},246:{T:-1},247:{T:1},248:{T:-1},249:{T:1},250:{T:-1},251:{T:1},252:{T:-1},253:{T:1},254:{T:-1},255:{T:1},256:{T:-1},257:{T:1},258:{T:-1},259:{T:1},260:{T:-1},261:{T:1},262:{T:-1},263:{T:1},264:{T:-1},265:{T:1},266:{T:-1},267:{T:1},268:{T:-1},269:{T:1},270:{T:-1},271:{T:1},272:{T:-1},273:{T:1},274:{T:-1},275:{T:1},276:{T:-1},277:{},278:{T:1},279:{T:-1},280:{T:1},281:{T:-1},282:{T:1},283:{T:1},284:{T:-1},285:{T:1},286:{T:-1},287:{T:1},288:{T:-1},289:{T:1},290:{T:-1},291:{T:1},292:{T:-1},293:{T:1},294:{T:-1},295:{T:1},296:{T:-1},297:{T:1},298:{T:-1},299:{T:1},300:{T:-1},301:{T:1},302:{T:-1},303:{T:1},304:{T:-1},305:{T:1},306:{T:-1},307:{T:1},308:{T:-1},309:{T:1},310:{T:-1},311:{T:1},312:{T:-1},313:{T:-1},314:{T:1},315:{T:-1},316:{T:1},317:{T:-1},318:{T:1},319:{T:-1},320:{T:1},321:{T:-1},322:{T:1},323:{T:-1},324:{T:1},325:{T:-1},326:{T:1},327:{T:-1},328:{T:1},329:{T:-1},330:{T:1},331:{T:-1},332:{T:1},333:{T:-1},334:{T:1},335:{f:Ew},336:{T:-1},337:{f:kw,T:1},338:{T:-1},339:{T:1},340:{T:-1},341:{T:1},342:{T:-1},343:{T:1},344:{T:-1},345:{T:1},346:{T:-1},347:{T:1},348:{T:-1},349:{T:1},350:{T:-1},351:{},352:{},353:{T:1},354:{T:-1},355:{f:hl},357:{},358:{},359:{},360:{T:1},361:{},362:{f:Oy},363:{},364:{},366:{},367:{},368:{},369:{},370:{},371:{},372:{T:1},373:{T:-1},374:{T:1},375:{T:-1},376:{T:1},377:{T:-1},378:{T:1},379:{T:-1},380:{T:1},381:{T:-1},382:{T:1},383:{T:-1},384:{T:1},385:{T:-1},386:{T:1},387:{T:-1},388:{T:1},389:{T:-1},390:{T:1},391:{T:-1},392:{T:1},393:{T:-1},394:{T:1},395:{T:-1},396:{},397:{},398:{},399:{},400:{},401:{T:1},403:{},404:{},405:{},406:{},407:{},408:{},409:{},410:{},411:{},412:{},413:{},414:{},415:{},416:{},417:{},418:{},419:{},420:{},421:{},422:{T:1},423:{T:1},424:{T:-1},425:{T:-1},426:{f:yT},427:{f:wT},428:{},429:{T:1},430:{T:-1},431:{T:1},432:{T:-1},433:{T:1},434:{T:-1},435:{T:1},436:{T:-1},437:{T:1},438:{T:-1},439:{T:1},440:{T:-1},441:{T:1},442:{T:-1},443:{T:1},444:{T:-1},445:{T:1},446:{T:-1},447:{T:1},448:{T:-1},449:{T:1},450:{T:-1},451:{T:1},452:{T:-1},453:{T:1},454:{T:-1},455:{T:1},456:{T:-1},457:{T:1},458:{T:-1},459:{T:1},460:{T:-1},461:{T:1},462:{T:-1},463:{T:1},464:{T:-1},465:{T:1},466:{T:-1},467:{T:1},468:{T:-1},469:{T:1},470:{T:-1},471:{},472:{},473:{T:1},474:{T:-1},475:{},476:{f:TT},477:{},478:{},479:{T:1},480:{T:-1},481:{T:1},482:{T:-1},483:{T:1},484:{T:-1},485:{f:OS},486:{T:1},487:{T:-1},488:{T:1},489:{T:-1},490:{T:1},491:{T:-1},492:{T:1},493:{T:-1},494:{f:gT},495:{T:1},496:{T:-1},497:{T:1},498:{T:-1},499:{},500:{T:1},501:{T:-1},502:{T:1},503:{T:-1},504:{},505:{T:1},506:{T:-1},507:{},508:{T:1},509:{T:-1},510:{T:1},511:{T:-1},512:{},513:{},514:{T:1},515:{T:-1},516:{T:1},517:{T:-1},518:{T:1},519:{T:-1},520:{T:1},521:{T:-1},522:{},523:{},524:{},525:{},526:{},527:{},528:{T:1},529:{T:-1},530:{T:1},531:{T:-1},532:{T:1},533:{T:-1},534:{},535:{},536:{},537:{},538:{T:1},539:{T:-1},540:{T:1},541:{T:-1},542:{T:1},548:{},549:{},550:{f:hl},551:{},552:{},553:{},554:{T:1},555:{T:-1},556:{T:1},557:{T:-1},558:{T:1},559:{T:-1},560:{T:1},561:{T:-1},562:{},564:{},565:{T:1},566:{T:-1},569:{T:1},570:{T:-1},572:{},573:{T:1},574:{T:-1},577:{},578:{},579:{},580:{},581:{},582:{},583:{},584:{},585:{},586:{},587:{},588:{T:-1},589:{},590:{T:1},591:{T:-1},592:{T:1},593:{T:-1},594:{T:1},595:{T:-1},596:{},597:{T:1},598:{T:-1},599:{T:1},600:{T:-1},601:{T:1},602:{T:-1},603:{T:1},604:{T:-1},605:{T:1},606:{T:-1},607:{},608:{T:1},609:{T:-1},610:{},611:{T:1},612:{T:-1},613:{T:1},614:{T:-1},615:{T:1},616:{T:-1},617:{T:1},618:{T:-1},619:{T:1},620:{T:-1},625:{},626:{T:1},627:{T:-1},628:{T:1},629:{T:-1},630:{T:1},631:{T:-1},632:{f:Mw},633:{T:1},634:{T:-1},635:{T:1,f:Nw},636:{T:-1},637:{f:zv},638:{T:1},639:{},640:{T:-1},641:{T:1},642:{T:-1},643:{T:1},644:{},645:{T:-1},646:{T:1},648:{T:1},649:{},650:{T:-1},651:{f:$T},652:{},653:{T:1},654:{T:-1},655:{T:1},656:{T:-1},657:{T:1},658:{T:-1},659:{},660:{T:1},661:{},662:{T:-1},663:{},664:{T:1},665:{},666:{T:-1},667:{},668:{},669:{},671:{T:1},672:{T:-1},673:{T:1},674:{T:-1},675:{},676:{},677:{},678:{},679:{},680:{},681:{},1024:{},1025:{},1026:{T:1},1027:{T:-1},1028:{T:1},1029:{T:-1},1030:{},1031:{T:1},1032:{T:-1},1033:{T:1},1034:{T:-1},1035:{},1036:{},1037:{},1038:{T:1},1039:{T:-1},1040:{},1041:{T:1},1042:{T:-1},1043:{},1044:{},1045:{},1046:{T:1},1047:{T:-1},1048:{T:1},1049:{T:-1},1050:{},1051:{T:1},1052:{T:1},1053:{f:kT},1054:{T:1},1055:{},1056:{T:1},1057:{T:-1},1058:{T:1},1059:{T:-1},1061:{},1062:{T:1},1063:{T:-1},1064:{T:1},1065:{T:-1},1066:{T:1},1067:{T:-1},1068:{T:1},1069:{T:-1},1070:{T:1},1071:{T:-1},1072:{T:1},1073:{T:-1},1075:{T:1},1076:{T:-1},1077:{T:1},1078:{T:-1},1079:{T:1},1080:{T:-1},1081:{T:1},1082:{T:-1},1083:{T:1},1084:{T:-1},1085:{},1086:{T:1},1087:{T:-1},1088:{T:1},1089:{T:-1},1090:{T:1},1091:{T:-1},1092:{T:1},1093:{T:-1},1094:{T:1},1095:{T:-1},1096:{},1097:{T:1},1098:{},1099:{T:-1},1100:{T:1},1101:{T:-1},1102:{},1103:{},1104:{},1105:{},1111:{},1112:{},1113:{T:1},1114:{T:-1},1115:{T:1},1116:{T:-1},1117:{},1118:{T:1},1119:{T:-1},1120:{T:1},1121:{T:-1},1122:{T:1},1123:{T:-1},1124:{T:1},1125:{T:-1},1126:{},1128:{T:1},1129:{T:-1},1130:{},1131:{T:1},1132:{T:-1},1133:{T:1},1134:{T:-1},1135:{T:1},1136:{T:-1},1137:{T:1},1138:{T:-1},1139:{T:1},1140:{T:-1},1141:{},1142:{T:1},1143:{T:-1},1144:{T:1},1145:{T:-1},1146:{},1147:{T:1},1148:{T:-1},1149:{T:1},1150:{T:-1},1152:{T:1},1153:{T:-1},1154:{T:-1},1155:{T:-1},1156:{T:-1},1157:{T:1},1158:{T:-1},1159:{T:1},1160:{T:-1},1161:{T:1},1162:{T:-1},1163:{T:1},1164:{T:-1},1165:{T:1},1166:{T:-1},1167:{T:1},1168:{T:-1},1169:{T:1},1170:{T:-1},1171:{},1172:{T:1},1173:{T:-1},1177:{},1178:{T:1},1180:{},1181:{},1182:{},2048:{T:1},2049:{T:-1},2050:{},2051:{T:1},2052:{T:-1},2053:{},2054:{},2055:{T:1},2056:{T:-1},2057:{T:1},2058:{T:-1},2060:{},2067:{},2068:{T:1},2069:{T:-1},2070:{},2071:{},2072:{T:1},2073:{T:-1},2075:{},2076:{},2077:{T:1},2078:{T:-1},2079:{},2080:{T:1},2081:{T:-1},2082:{},2083:{T:1},2084:{T:-1},2085:{T:1},2086:{T:-1},2087:{T:1},2088:{T:-1},2089:{T:1},2090:{T:-1},2091:{},2092:{},2093:{T:1},2094:{T:-1},2095:{},2096:{T:1},2097:{T:-1},2098:{T:1},2099:{T:-1},2100:{T:1},2101:{T:-1},2102:{},2103:{T:1},2104:{T:-1},2105:{},2106:{T:1},2107:{T:-1},2108:{},2109:{T:1},2110:{T:-1},2111:{T:1},2112:{T:-1},2113:{T:1},2114:{T:-1},2115:{},2116:{},2117:{},2118:{T:1},2119:{T:-1},2120:{},2121:{T:1},2122:{T:-1},2123:{T:1},2124:{T:-1},2125:{},2126:{T:1},2127:{T:-1},2128:{},2129:{T:1},2130:{T:-1},2131:{T:1},2132:{T:-1},2133:{T:1},2134:{},2135:{},2136:{},2137:{T:1},2138:{T:-1},2139:{T:1},2140:{T:-1},2141:{},3072:{},3073:{},4096:{T:1},4097:{T:-1},5002:{T:1},5003:{T:-1},5081:{T:1},5082:{T:-1},5083:{},5084:{T:1},5085:{T:-1},5086:{T:1},5087:{T:-1},5088:{},5089:{},5090:{},5092:{T:1},5093:{T:-1},5094:{},5095:{T:1},5096:{T:-1},5097:{},5099:{},65535:{n:""}};function Oe(e,t,r,n){var s=t;if(!isNaN(s)){var i=n||(r||[]).length||0,o=e.next(4);o.write_shift(2,s),o.write_shift(2,i),i>0&&$l(r)&&e.push(r)}}function Eb(e,t,r,n){var s=(r||[]).length||0;if(s<=8224)return Oe(e,t,r,s);var i=t;if(!isNaN(i)){for(var o=r.parts||[],l=0,c=0,d=0;d+(o[l]||8224)<=8224;)d+=o[l]||8224,l++;var u=e.next(4);for(u.write_shift(2,i),u.write_shift(2,d),e.push(r.slice(c,c+d)),c+=d;c=0&&s<65536?Oe(e,2,$y(r,n,s)):Oe(e,3,zy(r,n,s));return;case"b":case"e":Oe(e,5,jb(r,n,t.v,t.t));return;case"s":case"str":Oe(e,4,Ib(r,n,(t.v||"").slice(0,255)));return}Oe(e,1,Ba(null,r,n))}function Ab(e,t,r,n){var s=Array.isArray(t),i=vt(t["!ref"]||"A1"),o,l="",c=[];if(i.e.c>255||i.e.r>16383){if(n.WTF)throw new Error("Range "+(t["!ref"]||"A1")+" exceeds format limit A1:IV16384");i.e.c=Math.min(i.e.c,255),i.e.r=Math.min(i.e.c,16383),o=Ot(i)}for(var d=i.s.r;d<=i.e.r;++d){l=Jt(d);for(var u=i.s.c;u<=i.e.c;++u){d===i.s.r&&(c[u]=sr(u)),o=c[u]+l;var h=s?(t[d]||[])[u]:t[o];h&&Cb(e,h,d,u)}}}function kb(e,t){for(var r=t||{},n=xr(),s=0,i=0;i255||_.e.r>=x){if(t.WTF)throw new Error("Range "+(i["!ref"]||"A1")+" exceeds format limit A1:IV16384");_.e.c=Math.min(_.e.c,255),_.e.r=Math.min(_.e.c,x-1)}Oe(n,2057,Yl(r,16,t)),Oe(n,13,Nr(1)),Oe(n,12,Nr(100)),Oe(n,15,or(!0)),Oe(n,17,or(!1)),Oe(n,16,Zn(.001)),Oe(n,95,or(!0)),Oe(n,42,or(!1)),Oe(n,43,or(!1)),Oe(n,130,Nr(1)),Oe(n,128,Ay()),Oe(n,131,or(!1)),Oe(n,132,or(!1)),d&&Nb(n,i["!cols"]),Oe(n,512,Cy(_,t)),d&&(i["!links"]=[]);for(var f=_.s.r;f<=_.e.r;++f){h=Jt(f);for(var p=_.s.c;p<=_.e.c;++p){f===_.s.r&&(m[p]=sr(p)),u=m[p]+h;var I=c?(i[f]||[])[p]:i[u];I&&(Lb(n,I,f,p,t),d&&I.l&&i["!links"].push([u,I.l]))}}var S=l.CodeName||l.name||s;return d&&Oe(n,574,Ty((o.Views||[])[0])),d&&(i["!merges"]||[]).length&&Oe(n,229,Dy(i["!merges"])),d&&Pb(n,i),Oe(n,442,Gf(S)),d&&Rb(n,i),Oe(n,10),n.end()}function Bb(e,t,r){var n=xr(),s=(e||{}).Workbook||{},i=s.Sheets||[],o=s.WBProps||{},l=r.biff==8,c=r.biff==5;if(Oe(n,2057,Yl(e,5,r)),r.bookType=="xla"&&Oe(n,135),Oe(n,225,l?Nr(1200):null),Oe(n,193,dy(2)),c&&Oe(n,191),c&&Oe(n,192),Oe(n,226),Oe(n,92,vy("SheetJS",r)),Oe(n,66,Nr(l?1200:1252)),l&&Oe(n,353,Nr(0)),l&&Oe(n,448),Oe(n,317,Uy(e.SheetNames.length)),l&&e.vbaraw&&Oe(n,211),l&&e.vbaraw){var d=o.CodeName||"ThisWorkbook";Oe(n,442,Gf(d))}Oe(n,156,Nr(17)),Oe(n,25,or(!1)),Oe(n,18,or(!1)),Oe(n,19,Nr(0)),l&&Oe(n,431,or(!1)),l&&Oe(n,444,Nr(0)),Oe(n,61,Sy()),Oe(n,64,or(!1)),Oe(n,141,Nr(0)),Oe(n,34,or(HT(e)=="true")),Oe(n,14,or(!0)),l&&Oe(n,439,or(!1)),Oe(n,218,Nr(0)),Fb(n,e,r),Ob(n,e.SSF,r),Db(n,r),l&&Oe(n,352,or(!1));var u=n.end(),h=xr();l&&Oe(h,140,Ly()),l&&r.Strings&&Eb(h,252,wy(r.Strings)),Oe(h,10);var m=h.end(),_=xr(),x=0,f=0;for(f=0;f255&&typeof console<"u"&&console.error&&console.error("Worksheet '"+e.SheetNames[r]+"' extends beyond column IV (255). Data may be lost.")}}var i=t||{};switch(i.biff||2){case 8:case 5:return Ub(e,t);case 4:case 3:case 2:return kb(e,t)}throw new Error("invalid type "+i.bookType+" for BIFF")}function zb(e,t,r,n){for(var s=e["!merges"]||[],i=[],o=t.s.c;o<=t.e.c;++o){for(var l=0,c=0,d=0;dr||s[d].s.c>o)&&!(s[d].e.r1&&(_.rowspan=l),c>1&&(_.colspan=c),n.editable?m=''+m+"":h&&(_["data-t"]=h&&h.t||"z",h.v!=null&&(_["data-v"]=h.v),h.z!=null&&(_["data-z"]=h.z),h.l&&(h.l.Target||"#").charAt(0)!="#"&&(m=''+m+"")),_.id=(n.id||"sjs")+"-"+u,i.push(Fe("td",m,_))}}var x="";return x+i.join("")+""}var $b='SheetJS Table Export',Wb="";function Hb(e,t,r){var n=[];return n.join("")+""}function wh(e,t){var r=t||{},n=r.header!=null?r.header:$b,s=r.footer!=null?r.footer:Wb,i=[n],o=Ir(e["!ref"]);r.dense=Array.isArray(e),i.push(Hb(e,o,r));for(var l=o.s.r;l<=o.e.r;++l)i.push(zb(e,o,l,r));return i.push(""+s),i.join("")}function Sh(e,t,r){var n=r||{},s=0,i=0;if(n.origin!=null)if(typeof n.origin=="number")s=n.origin;else{var o=typeof n.origin=="string"?zt(n.origin):n.origin;s=o.r,i=o.c}var l=t.getElementsByTagName("tr"),c=Math.min(n.sheetRows||1e7,l.length),d={s:{r:0,c:0},e:{r:s,c:i}};if(e["!ref"]){var u=Ir(e["!ref"]);d.s.r=Math.min(d.s.r,u.s.r),d.s.c=Math.min(d.s.c,u.s.c),d.e.r=Math.max(d.e.r,u.e.r),d.e.c=Math.max(d.e.c,u.e.c),s==-1&&(d.e.r=s=u.e.r+1)}var h=[],m=0,_=e["!rows"]||(e["!rows"]=[]),x=0,f=0,p=0,I=0,S=0,w=0;for(e["!cols"]||(e["!cols"]=[]);x1||w>1)&&h.push({s:{r:f+s,c:I+i},e:{r:f+s+(S||1)-1,c:I+i+(w||1)-1}});var F={t:"s",v:j},P=R.getAttribute("data-t")||R.getAttribute("t")||"";j!=null&&(j.length==0?F.t=P||"z":n.raw||j.trim().length==0||P=="s"||(j==="TRUE"?F={t:"b",v:!0}:j==="FALSE"?F={t:"b",v:!1}:isNaN(pn(j))?isNaN(va(j).getDate())||(F={t:"d",v:lr(j)},n.cellDates||(F={t:"n",v:pr(F.v)}),F.z=n.dateNF||jt[14]):F={t:"n",v:pn(j)})),F.z===void 0&&T!=null&&(F.z=T);var V="",X=R.getElementsByTagName("A");if(X&&X.length)for(var _e=0;_e=c&&(e["!fullref"]=Ot((d.e.r=l.length-x+f-1+s,d))),e}function Th(e,t){var r=t||{},n=r.dense?[]:{};return Sh(n,e,t)}function Vb(e,t){return es(Th(e,t),t)}function Kd(e){var t="",r=Kb(e);return r&&(t=r(e).getPropertyValue("display")),t||(t=e.style&&e.style.display),t==="none"}function Kb(e){return e.ownerDocument.defaultView&&typeof e.ownerDocument.defaultView.getComputedStyle=="function"?e.ownerDocument.defaultView.getComputedStyle:typeof getComputedStyle=="function"?getComputedStyle:null}var Gb=function(){var e=["",'',"",'',"",'',"",""].join(""),t=""+e+"";return function(){return Rt+t}}(),Gd=function(){var e=function(i){return ht(i).replace(/ +/g,function(o){return''}).replace(/\t/g,"").replace(/\n/g,"").replace(/^ /,"").replace(/ $/,"")},t=` +`,r=` +`,n=function(i,o,l){var c=[];c.push(' +`);var d=0,u=0,h=Ir(i["!ref"]||"A1"),m=i["!merges"]||[],_=0,x=Array.isArray(i);if(i["!cols"])for(u=0;u<=h.e.c;++u)c.push(" +`);var f="",p=i["!rows"]||[];for(d=0;d +`);for(;d<=h.e.r;++d){for(f=p[d]?' table:style-name="ro'+p[d].ods+'"':"",c.push(" +`),u=0;uu)&&!(m[_].s.r>d)&&!(m[_].e.c +`)}return c.push(` +`),c.join("")},s=function(i,o){i.push(` +`),i.push(` +`),i.push(` +`),i.push(` / +`),i.push(` +`),i.push(` / +`),i.push(` +`),i.push(` +`);var l=0;o.SheetNames.map(function(d){return o.Sheets[d]}).forEach(function(d){if(d&&d["!cols"]){for(var u=0;u +`),i.push(' +`),i.push(` +`),++l}}});var c=0;o.SheetNames.map(function(d){return o.Sheets[d]}).forEach(function(d){if(d&&d["!rows"]){for(var u=0;u +`),i.push(' +`),i.push(` +`),++c}}}),i.push(` +`),i.push(` +`),i.push(` +`),i.push(` +`),i.push(` +`)};return function(o,l){var c=[Rt],d=wa({"xmlns:office":"urn:oasis:names:tc:opendocument:xmlns:office:1.0","xmlns:table":"urn:oasis:names:tc:opendocument:xmlns:table:1.0","xmlns:style":"urn:oasis:names:tc:opendocument:xmlns:style:1.0","xmlns:text":"urn:oasis:names:tc:opendocument:xmlns:text:1.0","xmlns:draw":"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","xmlns:fo":"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","xmlns:xlink":"http://www.w3.org/1999/xlink","xmlns:dc":"http://purl.org/dc/elements/1.1/","xmlns:meta":"urn:oasis:names:tc:opendocument:xmlns:meta:1.0","xmlns:number":"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0","xmlns:presentation":"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0","xmlns:svg":"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0","xmlns:chart":"urn:oasis:names:tc:opendocument:xmlns:chart:1.0","xmlns:dr3d":"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0","xmlns:math":"http://www.w3.org/1998/Math/MathML","xmlns:form":"urn:oasis:names:tc:opendocument:xmlns:form:1.0","xmlns:script":"urn:oasis:names:tc:opendocument:xmlns:script:1.0","xmlns:ooo":"http://openoffice.org/2004/office","xmlns:ooow":"http://openoffice.org/2004/writer","xmlns:oooc":"http://openoffice.org/2004/calc","xmlns:dom":"http://www.w3.org/2001/xml-events","xmlns:xforms":"http://www.w3.org/2002/xforms","xmlns:xsd":"http://www.w3.org/2001/XMLSchema","xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance","xmlns:sheet":"urn:oasis:names:tc:opendocument:sh33tjs:1.0","xmlns:rpt":"http://openoffice.org/2005/report","xmlns:of":"urn:oasis:names:tc:opendocument:xmlns:of:1.2","xmlns:xhtml":"http://www.w3.org/1999/xhtml","xmlns:grddl":"http://www.w3.org/2003/g/data-view#","xmlns:tableooo":"http://openoffice.org/2009/table","xmlns:drawooo":"http://openoffice.org/2010/draw","xmlns:calcext":"urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0","xmlns:loext":"urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0","xmlns:field":"urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0","xmlns:formx":"urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0","xmlns:css3t":"http://www.w3.org/TR/css3-text/","office:version":"1.2"}),u=wa({"xmlns:config":"urn:oasis:names:tc:opendocument:xmlns:config:1.0","office:mimetype":"application/vnd.oasis.opendocument.spreadsheet"});l.bookType=="fods"?(c.push(" +`),c.push(Mf().replace(/office:document-meta/g,"office:meta"))):c.push(" +`),s(c,o),c.push(` +`),c.push(` +`);for(var h=0;h!=o.SheetNames.length;++h)c.push(n(o.Sheets[o.SheetNames[h]],o,h));return c.push(` +`),c.push(` +`),l.bookType=="fods"?c.push(""):c.push(""),c.join("")}}();function bh(e,t){if(t.bookType=="fods")return Gd(e,t);var r=Ml(),n="",s=[],i=[];return n="mimetype",Ze(r,n,"application/vnd.oasis.opendocument.spreadsheet"),n="content.xml",Ze(r,n,Gd(e,t)),s.push([n,"text/xml"]),i.push([n,"ContentFile"]),n="styles.xml",Ze(r,n,Gb(e,t)),s.push([n,"text/xml"]),i.push([n,"StylesFile"]),n="meta.xml",Ze(r,n,Rt+Mf()),s.push([n,"text/xml"]),i.push([n,"MetadataFile"]),n="manifest.rdf",Ze(r,n,ay(i)),s.push([n,"application/rdf+xml"]),n="META-INF/manifest.xml",Ze(r,n,ny(s)),r}/*! sheetjs (C) 2013-present SheetJS -- http://sheetjs.com */function Bi(e){return new DataView(e.buffer,e.byteOffset,e.byteLength)}function Yb(e){return typeof TextEncoder<"u"?new TextEncoder().encode(e):Wr(ya(e))}function qb(e,t){e:for(var r=0;r<=e.length-t.length;++r){for(var n=0;n>7,e[t+14]|=(n&127)<<1;for(var i=0;s>=1;++i,s/=256)e[t+i]=s&255;e[t+15]|=r>=0?0:128}function Sa(e,t){var r=t?t[0]:0,n=e[r]&127;e:if(e[r++]>=128&&(n|=(e[r]&127)<<7,e[r++]<128||(n|=(e[r]&127)<<14,e[r++]<128)||(n|=(e[r]&127)<<21,e[r++]<128)||(n+=(e[r]&127)*Math.pow(2,28),++r,e[r++]<128)||(n+=(e[r]&127)*Math.pow(2,35),++r,e[r++]<128)||(n+=(e[r]&127)*Math.pow(2,42),++r,e[r++]<128)))break e;return t&&(t[0]=r),n}function ut(e){var t=new Uint8Array(7);t[0]=e&127;var r=1;e:if(e>127){if(t[r-1]|=128,t[r]=e>>7&127,++r,e<=16383||(t[r-1]|=128,t[r]=e>>14&127,++r,e<=2097151)||(t[r-1]|=128,t[r]=e>>21&127,++r,e<=268435455)||(t[r-1]|=128,t[r]=e/256>>>21&127,++r,e<=34359738367)||(t[r-1]|=128,t[r]=e/65536>>>21&127,++r,e<=4398046511103))break e;t[r-1]|=128,t[r]=e/16777216>>>21&127,++r}return t.slice(0,r)}function bs(e){var t=0,r=e[t]&127;e:if(e[t++]>=128){if(r|=(e[t]&127)<<7,e[t++]<128||(r|=(e[t]&127)<<14,e[t++]<128)||(r|=(e[t]&127)<<21,e[t++]<128))break e;r|=(e[t]&127)<<28}return r}function Nt(e){for(var t=[],r=[0];r[0]=128;);l=e.slice(c,r[0])}break;case 5:o=4,l=e.slice(r[0],r[0]+o),r[0]+=o;break;case 1:o=8,l=e.slice(r[0],r[0]+o),r[0]+=o;break;case 2:o=Sa(e,r),l=e.slice(r[0],r[0]+o),r[0]+=o;break;case 3:case 4:default:throw new Error("PB Type ".concat(i," for Field ").concat(s," at offset ").concat(n))}var d={data:l,type:i};t[s]==null?t[s]=[d]:t[s].push(d)}return t}function Vt(e){var t=[];return e.forEach(function(r,n){r.forEach(function(s){s.data&&(t.push(ut(n*8+s.type)),s.type==2&&t.push(ut(s.data.length)),t.push(s.data))})}),Ln(t)}function Ur(e){for(var t,r=[],n=[0];n[0]>>0>0),r.push(o)}return r}function fs(e){var t=[];return e.forEach(function(r){var n=[];n[1]=[{data:ut(r.id),type:0}],n[2]=[],r.merge!=null&&(n[3]=[{data:ut(+!!r.merge),type:0}]);var s=[];r.messages.forEach(function(o){s.push(o.data),o.meta[3]=[{type:0,data:ut(o.data.length)}],n[2].push({data:Vt(o.meta),type:2})});var i=Vt(n);t.push(ut(i.length)),t.push(i),s.forEach(function(o){return t.push(o)})}),Ln(t)}function Jb(e,t){if(e!=0)throw new Error("Unexpected Snappy chunk type ".concat(e));for(var r=[0],n=Sa(t,r),s=[];r[0]>2;if(o<60)++o;else{var l=o-59;o=t[r[0]],l>1&&(o|=t[r[0]+1]<<8),l>2&&(o|=t[r[0]+2]<<16),l>3&&(o|=t[r[0]+3]<<24),o>>>=0,o++,r[0]+=l}s.push(t.slice(r[0],r[0]+o)),r[0]+=o;continue}else{var c=0,d=0;if(i==1?(d=(t[r[0]]>>2&7)+4,c=(t[r[0]++]&224)<<3,c|=t[r[0]++]):(d=(t[r[0]++]>>2)+1,i==2?(c=t[r[0]]|t[r[0]+1]<<8,r[0]+=2):(c=(t[r[0]]|t[r[0]+1]<<8|t[r[0]+2]<<16|t[r[0]+3]<<24)>>>0,r[0]+=4)),s=[Ln(s)],c==0)throw new Error("Invalid offset 0");if(c>s[0].length)throw new Error("Invalid offset beyond length");if(d>=c)for(s.push(s[0].slice(-c)),d-=c;d>=s[s.length-1].length;)s.push(s[s.length-1]),d-=s[s.length-1].length;s.push(s[0].slice(-c,-c+d))}}var u=Ln(s);if(u.length!=n)throw new Error("Unexpected length: ".concat(u.length," != ").concat(n));return u}function zr(e){for(var t=[],r=0;r>8&255]))):n<=16777216?(o+=4,t.push(new Uint8Array([248,n-1&255,n-1>>8&255,n-1>>16&255]))):n<=4294967296&&(o+=5,t.push(new Uint8Array([252,n-1&255,n-1>>8&255,n-1>>16&255,n-1>>>24&255]))),t.push(e.slice(r,r+n)),o+=n,s[0]=0,s[1]=o&255,s[2]=o>>8&255,s[3]=o>>16&255,r+=n}return Ln(t)}function Uo(e,t){var r=new Uint8Array(32),n=Bi(r),s=12,i=0;switch(r[0]=5,e.t){case"n":r[1]=2,Xb(r,s,e.v),i|=1,s+=16;break;case"b":r[1]=6,n.setFloat64(s,e.v?1:0,!0),i|=2,s+=8;break;case"s":if(t.indexOf(e.v)==-1)throw new Error("Value ".concat(e.v," missing from SST!"));r[1]=3,n.setUint32(s,t.indexOf(e.v),!0),i|=8,s+=4;break;default:throw"unsupported cell type "+e.t}return n.setUint32(8,i,!0),r.slice(0,s)}function zo(e,t){var r=new Uint8Array(32),n=Bi(r),s=12,i=0;switch(r[0]=3,e.t){case"n":r[2]=2,n.setFloat64(s,e.v,!0),i|=32,s+=8;break;case"b":r[2]=6,n.setFloat64(s,e.v?1:0,!0),i|=32,s+=8;break;case"s":if(t.indexOf(e.v)==-1)throw new Error("Value ".concat(e.v," missing from SST!"));r[2]=3,n.setUint32(s,t.indexOf(e.v),!0),i|=16,s+=4;break;default:throw"unsupported cell type "+e.t}return n.setUint32(4,i,!0),r.slice(0,s)}function jn(e){var t=Nt(e);return Sa(t[1][0].data)}function Zb(e,t,r){var n,s,i,o;if(!((n=e[6])!=null&&n[0])||!((s=e[7])!=null&&s[0]))throw"Mutation only works on post-BNC storages!";var l=((o=(i=e[8])==null?void 0:i[0])==null?void 0:o.data)&&bs(e[8][0].data)>0||!1;if(l)throw"Math only works with normal offsets";for(var c=0,d=Bi(e[7][0].data),u=0,h=[],m=Bi(e[4][0].data),_=0,x=[],f=0;f1&&console.error("The Numbers writer currently writes only the first table");var n=Ir(r["!ref"]);n.s.r=n.s.c=0;var s=!1;n.e.c>9&&(s=!0,n.e.c=9),n.e.r>49&&(s=!0,n.e.r=49),s&&console.error("The Numbers writer is currently limited to ".concat(Ot(n)));var i=Ui(r,{range:n,header:1}),o=["~Sh33tJ5~"];i.forEach(function(W){return W.forEach(function(z){typeof z=="string"&&o.push(z)})});var l={},c=[],d=xt.read(t.numbers,{type:"base64"});d.FileIndex.map(function(W,z){return[W,d.FullPaths[z]]}).forEach(function(W){var z=W[0],D=W[1];if(z.type==2&&z.name.match(/\.iwa/)){var q=z.content,K=zr(q),Z=Ur(K);Z.forEach(function(me){c.push(me.id),l[me.id]={deps:[],location:D,type:bs(me.messages[0].meta[1][0].data)}})}}),c.sort(function(W,z){return W-z});var u=c.filter(function(W){return W>1}).map(function(W){return[W,ut(W)]});d.FileIndex.map(function(W,z){return[W,d.FullPaths[z]]}).forEach(function(W){var z=W[0];if(W[1],!!z.name.match(/\.iwa/)){var D=Ur(zr(z.content));D.forEach(function(q){q.messages.forEach(function(K){u.forEach(function(Z){q.messages.some(function(me){return bs(me.meta[1][0].data)!=11006&&qb(me.data,Z[1])})&&l[Z[0]].deps.push(q.id)})})})}});for(var h=xt.find(d,l[1].location),m=Ur(zr(h.content)),_,x=0;x-1,s=Pf();ec(t=t||{});var i=Ml(),o="",l=0;if(t.cellXfs=[],Bn(t.cellXfs,{},{revssf:{General:0}}),e.Props||(e.Props={}),o="docProps/core.xml",Ze(i,o,Bf(e.Props,t)),s.coreprops.push(o),ft(t.rels,2,o,it.CORE_PROPS),o="docProps/app.xml",!(e.Props&&e.Props.SheetNames))if(!e.Workbook||!e.Workbook.Sheets)e.Props.SheetNames=e.SheetNames;else{for(var c=[],d=0;d0&&(o="docProps/custom.xml",Ze(i,o,$f(e.Custprops)),s.custprops.push(o),ft(t.rels,4,o,it.CUST_PROPS)),l=1;l<=e.SheetNames.length;++l){var u={"!id":{}},h=e.Sheets[e.SheetNames[l-1]],m=(h||{})["!type"]||"sheet";switch(m){case"chart":default:o="xl/worksheets/sheet"+l+"."+r,Ze(i,o,ab(l-1,o,t,e,u)),s.sheets.push(o),ft(t.wbrels,-1,"worksheets/sheet"+l+"."+r,it.WS[0])}if(h){var _=h["!comments"],x=!1,f="";_&&_.length>0&&(f="xl/comments"+l+"."+r,Ze(i,f,lb(_,f)),s.comments.push(f),ft(u,-1,"../comments"+l+"."+r,it.CMNT),x=!0),h["!legacy"]&&x&&Ze(i,"xl/drawings/vmlDrawing"+l+".vml",sh(l,h["!comments"])),delete h["!comments"],delete h["!legacy"]}u["!id"].rId1&&Ze(i,Lf(o),Ss(u))}return t.Strings!=null&&t.Strings.length>0&&(o="xl/sharedStrings."+r,Ze(i,o,ob(t.Strings,o,t)),s.strs.push(o),ft(t.wbrels,-1,"sharedStrings."+r,it.SST)),o="xl/workbook."+r,Ze(i,o,sb(e,o)),s.workbooks.push(o),ft(t.rels,1,o,it.WB),o="xl/theme/theme1.xml",Ze(i,o,rh(e.Themes,t)),s.themes.push(o),ft(t.wbrels,-1,"theme/theme1.xml",it.THEME),o="xl/styles."+r,Ze(i,o,ib(e,o,t)),s.styles.push(o),ft(t.wbrels,-1,"styles."+r,it.STY),e.vbaraw&&n&&(o="xl/vbaProject.bin",Ze(i,o,e.vbaraw),s.vba.push(o),ft(t.wbrels,-1,"vbaProject.bin",it.VBA)),o="xl/metadata."+r,Ze(i,o,cb(o)),s.metadata.push(o),ft(t.wbrels,-1,"metadata."+r,it.XLMETA),Ze(i,"[Content_Types].xml",Nf(s,t)),Ze(i,"_rels/.rels",Ss(t.rels)),Ze(i,"xl/_rels/workbook."+r+".rels",Ss(t.wbrels)),delete t.revssf,delete t.ssf,i}function nE(e,t){_s=1024,e&&!e.SSF&&(e.SSF=gr(jt)),e&&e.SSF&&(co(),lo(e.SSF),t.revssf=uo(e.SSF),t.revssf[e.SSF[65535]]=0,t.ssf=e.SSF),t.rels={},t.wbrels={},t.Strings=[],t.Strings.Count=0,t.Strings.Unique=0,oa?t.revStrings=new Map:(t.revStrings={},t.revStrings.foo=[],delete t.revStrings.foo);var r="xml",n=ih.indexOf(t.bookType)>-1,s=Pf();ec(t=t||{});var i=Ml(),o="",l=0;if(t.cellXfs=[],Bn(t.cellXfs,{},{revssf:{General:0}}),e.Props||(e.Props={}),o="docProps/core.xml",Ze(i,o,Bf(e.Props,t)),s.coreprops.push(o),ft(t.rels,2,o,it.CORE_PROPS),o="docProps/app.xml",!(e.Props&&e.Props.SheetNames))if(!e.Workbook||!e.Workbook.Sheets)e.Props.SheetNames=e.SheetNames;else{for(var c=[],d=0;d0&&(o="docProps/custom.xml",Ze(i,o,$f(e.Custprops)),s.custprops.push(o),ft(t.rels,4,o,it.CUST_PROPS));var u=["SheetJ5"];for(t.tcid=0,l=1;l<=e.SheetNames.length;++l){var h={"!id":{}},m=e.Sheets[e.SheetNames[l-1]],_=(m||{})["!type"]||"sheet";switch(_){case"chart":default:o="xl/worksheets/sheet"+l+"."+r,Ze(i,o,xh(l-1,t,e,h)),s.sheets.push(o),ft(t.wbrels,-1,"worksheets/sheet"+l+"."+r,it.WS[0])}if(m){var x=m["!comments"],f=!1,p="";if(x&&x.length>0){var I=!1;x.forEach(function(S){S[1].forEach(function(w){w.T==!0&&(I=!0)})}),I&&(p="xl/threadedComments/threadedComment"+l+"."+r,Ze(i,p,Dw(x,u,t)),s.threadedcomments.push(p),ft(h,-1,"../threadedComments/threadedComment"+l+"."+r,it.TCMNT)),p="xl/comments"+l+"."+r,Ze(i,p,ah(x)),s.comments.push(p),ft(h,-1,"../comments"+l+"."+r,it.CMNT),f=!0}m["!legacy"]&&f&&Ze(i,"xl/drawings/vmlDrawing"+l+".vml",sh(l,m["!comments"])),delete m["!comments"],delete m["!legacy"]}h["!id"].rId1&&Ze(i,Lf(o),Ss(h))}return t.Strings!=null&&t.Strings.length>0&&(o="xl/sharedStrings."+r,Ze(i,o,Xf(t.Strings,t)),s.strs.push(o),ft(t.wbrels,-1,"sharedStrings."+r,it.SST)),o="xl/workbook."+r,Ze(i,o,_h(e)),s.workbooks.push(o),ft(t.rels,1,o,it.WB),o="xl/theme/theme1.xml",Ze(i,o,rh(e.Themes,t)),s.themes.push(o),ft(t.wbrels,-1,"theme/theme1.xml",it.THEME),o="xl/styles."+r,Ze(i,o,eh(e,t)),s.styles.push(o),ft(t.wbrels,-1,"styles."+r,it.STY),e.vbaraw&&n&&(o="xl/vbaProject.bin",Ze(i,o,e.vbaraw),s.vba.push(o),ft(t.wbrels,-1,"vbaProject.bin",it.VBA)),o="xl/metadata."+r,Ze(i,o,nh()),s.metadata.push(o),ft(t.wbrels,-1,"metadata."+r,it.XLMETA),u.length>1&&(o="xl/persons/person.xml",Ze(i,o,Pw(u)),s.people.push(o),ft(t.wbrels,-1,"persons/person.xml",it.PEOPLE)),Ze(i,"[Content_Types].xml",Nf(s,t)),Ze(i,"_rels/.rels",Ss(t.rels)),Ze(i,"xl/_rels/workbook."+r+".rels",Ss(t.wbrels)),delete t.revssf,delete t.ssf,i}function sE(e,t){var r="";switch((t||{}).type||"base64"){case"buffer":return[e[0],e[1],e[2],e[3],e[4],e[5],e[6],e[7]];case"base64":r=vn(e.slice(0,12));break;case"binary":r=e;break;case"array":return[e[0],e[1],e[2],e[3],e[4],e[5],e[6],e[7]];default:throw new Error("Unrecognized type "+(t&&t.type||"undefined"))}return[r.charCodeAt(0),r.charCodeAt(1),r.charCodeAt(2),r.charCodeAt(3),r.charCodeAt(4),r.charCodeAt(5),r.charCodeAt(6),r.charCodeAt(7)]}function Eh(e,t){switch(t.type){case"base64":case"binary":break;case"buffer":case"array":t.type="";break;case"file":return Pa(t.file,xt.write(e,{type:ct?"buffer":""}));case"string":throw new Error("'string' output type invalid for '"+t.bookType+"' files");default:throw new Error("Unrecognized type "+t.type)}return xt.write(e,t)}function aE(e,t){var r=gr(t||{}),n=tE(e,r);return iE(n,r)}function iE(e,t){var r={},n=ct?"nodebuffer":typeof Uint8Array<"u"?"array":"string";if(t.compression&&(r.compression="DEFLATE"),t.password)r.type=n;else switch(t.type){case"base64":r.type="base64";break;case"binary":r.type="string";break;case"string":throw new Error("'string' output type invalid for '"+t.bookType+"' files");case"buffer":case"file":r.type=n;break;default:throw new Error("Unrecognized type "+t.type)}var s=e.FullPaths?xt.write(e,{fileType:"zip",type:{nodebuffer:"buffer",string:"binary"}[r.type]||r.type,compression:!!t.compression}):e.generate(r);if(typeof Deno<"u"&&typeof s=="string"){if(t.type=="binary"||t.type=="base64")return s;s=new Uint8Array(oo(s))}return t.password&&typeof encrypt_agile<"u"?Eh(encrypt_agile(s,t.password),t):t.type==="file"?Pa(t.file,s):t.type=="string"?na(s):s}function oE(e,t){var r=t||{},n=Tb(e,r);return Eh(n,r)}function en(e,t,r){r||(r="");var n=r+e;switch(t.type){case"base64":return _a(ya(n));case"binary":return ya(n);case"string":return e;case"file":return Pa(t.file,n,"utf8");case"buffer":return ct?wn(n,"utf8"):typeof TextEncoder<"u"?new TextEncoder().encode(n):en(n,{type:"binary"}).split("").map(function(s){return s.charCodeAt(0)})}throw new Error("Unrecognized type "+t.type)}function lE(e,t){switch(t.type){case"base64":return _a(e);case"binary":return e;case"string":return e;case"file":return Pa(t.file,e,"binary");case"buffer":return ct?wn(e,"binary"):e.split("").map(function(r){return r.charCodeAt(0)})}throw new Error("Unrecognized type "+t.type)}function ii(e,t){switch(t.type){case"string":case"base64":case"binary":for(var r="",n=0;n0&&(s=0);var h=Jt(c.s.r),m=[],_=[],x=0,f=0,p=Array.isArray(e),I=c.s.r,S=0,w={};p&&!e[I]&&(e[I]=[]);var v=d.skipHidden&&e["!cols"]||[],y=d.skipHidden&&e["!rows"]||[];for(S=c.s.c;S<=c.e.c;++S)if(!(v[S]||{}).hidden)switch(m[S]=sr(S),r=p?e[I][S]:e[m[S]+h],n){case 1:i[S]=S-c.s.c;break;case 2:i[S]=m[S];break;case 3:i[S]=d.header[S-c.s.c];break;default:if(r==null&&(r={w:"__EMPTY",t:"s"}),l=o=yn(r,null,d),f=w[o]||0,!f)w[o]=1;else{do l=o+"_"+f++;while(w[l]);w[o]=f,w[l]=1}i[S]=l}for(I=c.s.r+s;I<=c.e.r;++I)if(!(y[I]||{}).hidden){var R=uE(e,c,I,m,n,i,p,d);(R.isempty===!1||(n===1?d.blankrows!==!1:d.blankrows))&&(_[x++]=R.row)}return _.length=x,_}var Yd=/"/g;function fE(e,t,r,n,s,i,o,l){for(var c=!0,d=[],u="",h=Jt(r),m=t.s.c;m<=t.e.c;++m)if(n[m]){var _=l.dense?(e[r]||[])[m]:e[n[m]+h];if(_==null)u="";else if(_.v!=null){c=!1,u=""+(l.rawNumbers&&_.t=="n"?_.v:yn(_,null,l));for(var x=0,f=0;x!==u.length;++x)if((f=u.charCodeAt(x))===s||f===i||f===34||l.forceQuotes){u='"'+u.replace(Yd,'""')+'"';break}u=="ID"&&(u='"ID"')}else _.f!=null&&!_.F?(c=!1,u="="+_.f,u.indexOf(",")>=0&&(u='"'+u.replace(Yd,'""')+'"')):u="";d.push(u)}return l.blankrows===!1&&c?null:d.join(o)}function tc(e,t){var r=[],n=t??{};if(e==null||e["!ref"]==null)return"";var s=vt(e["!ref"]),i=n.FS!==void 0?n.FS:",",o=i.charCodeAt(0),l=n.RS!==void 0?n.RS:` +`,c=l.charCodeAt(0),d=new RegExp((i=="|"?"\\|":i)+"+$"),u="",h=[];n.dense=Array.isArray(e);for(var m=n.skipHidden&&e["!cols"]||[],_=n.skipHidden&&e["!rows"]||[],x=s.s.c;x<=s.e.c;++x)(m[x]||{}).hidden||(h[x]=sr(x));for(var f=0,p=s.s.r;p<=s.e.r;++p)(_[p]||{}).hidden||(u=fE(e,s,p,h,o,c,i,n),u!=null&&(n.strip&&(u=u.replace(d,"")),(u||n.blankrows!==!1)&&r.push((f++?l:"")+u)));return delete n.dense,r.join("")}function Ih(e,t){t||(t={}),t.FS=" ",t.RS=` +`;var r=tc(e,t);return r}function hE(e){var t="",r,n="";if(e==null||e["!ref"]==null)return[];var s=vt(e["!ref"]),i="",o=[],l,c=[],d=Array.isArray(e);for(l=s.s.c;l<=s.e.c;++l)o[l]=sr(l);for(var u=s.s.r;u<=s.e.r;++u)for(i=Jt(u),l=s.s.c;l<=s.e.c;++l)if(t=o[l]+i,r=d?(e[u]||[])[l]:e[t],n="",r!==void 0){if(r.F!=null){if(t=r.F,!r.f)continue;n=r.f,t.indexOf(":")==-1&&(t=t+":"+t)}if(r.f!=null)n=r.f;else{if(r.t=="z")continue;if(r.t=="n"&&r.v!=null)n=""+r.v;else if(r.t=="b")n=r.v?"TRUE":"FALSE";else if(r.w!==void 0)n="'"+r.w;else{if(r.v===void 0)continue;r.t=="s"?n="'"+r.v:n=""+r.v}}c[c.length]=t+"="+n}return c}function Ch(e,t,r){var n=r||{},s=+!n.skipHeader,i=e||{},o=0,l=0;if(i&&n.origin!=null)if(typeof n.origin=="number")o=n.origin;else{var c=typeof n.origin=="string"?zt(n.origin):n.origin;o=c.r,l=c.c}var d,u={s:{c:0,r:0},e:{c:l,r:o+t.length-1+s}};if(i["!ref"]){var h=vt(i["!ref"]);u.e.c=Math.max(u.e.c,h.e.c),u.e.r=Math.max(u.e.r,h.e.r),o==-1&&(o=h.e.r+1,u.e.r=o+t.length-1+s)}else o==-1&&(o=0,u.e.r=t.length-1+s);var m=n.header||[],_=0;t.forEach(function(f,p){Zt(f).forEach(function(I){(_=m.indexOf(I))==-1&&(m[_=m.length]=I);var S=f[I],w="z",v="",y=mt({c:l+_,r:o+p+s});d=Ta(i,y),S&&typeof S=="object"&&!(S instanceof Date)?i[y]=S:(typeof S=="number"?w="n":typeof S=="boolean"?w="b":typeof S=="string"?w="s":S instanceof Date?(w="d",n.cellDates||(w="n",S=pr(S)),v=n.dateNF||jt[14]):S===null&&n.nullError&&(w="e",S=0),d?(d.t=w,d.v=S,delete d.w,delete d.R,v&&(d.z=v)):i[y]=d={t:w,v:S},v&&(d.z=v))})}),u.e.c=Math.max(u.e.c,l+m.length-1);var x=Jt(o);if(s)for(_=0;_=0&&e.SheetNames.length>t)return t;throw new Error("Cannot find sheet # "+t)}else if(typeof t=="string"){var r=e.SheetNames.indexOf(t);if(r>-1)return r;throw new Error("Cannot find sheet name |"+t+"|")}else throw new Error("Cannot find sheet |"+t+"|")}function pE(){return{SheetNames:[],Sheets:{}}}function gE(e,t,r,n){var s=1;if(!r)for(;s<=65535&&e.SheetNames.indexOf(r="Sheet"+s)!=-1;++s,r=void 0);if(!r||e.SheetNames.length>=65535)throw new Error("Too many worksheets");if(n&&e.SheetNames.indexOf(r)>=0){var i=r.match(/(^.*?)(\d+)$/);s=i&&+i[2]||0;var o=i&&i[1]||r;for(++s;s<=65535&&e.SheetNames.indexOf(r=o+s)!=-1;++s);}if(gh(r),e.SheetNames.indexOf(r)>=0)throw new Error("Worksheet with name |"+r+"| already exists!");return e.SheetNames.push(r),e.Sheets[r]=t,r}function _E(e,t,r){e.Workbook||(e.Workbook={}),e.Workbook.Sheets||(e.Workbook.Sheets=[]);var n=xE(e,t);switch(e.Workbook.Sheets[n]||(e.Workbook.Sheets[n]={}),r){case 0:case 1:case 2:break;default:throw new Error("Bad sheet visibility setting "+r)}e.Workbook.Sheets[n].Hidden=r}function vE(e,t){return e.z=t,e}function Ah(e,t,r){return t?(e.l={Target:t},r&&(e.l.Tooltip=r)):delete e.l,e}function yE(e,t,r){return Ah(e,"#"+t,r)}function wE(e,t,r){e.c||(e.c=[]),e.c.push({t,a:r||"SheetJS"})}function SE(e,t,r,n){for(var s=typeof t!="string"?t:vt(t),i=typeof t=="string"?t:Ot(t),o=s.s.r;o<=s.e.r;++o)for(var l=s.s.c;l<=s.e.c;++l){var c=Ta(e,o,l);c.t="n",c.F=i,delete c.v,o==s.s.r&&l==s.s.c&&(c.f=r,n&&(c.D=!0))}return e}var $o={encode_col:sr,encode_row:Jt,encode_cell:mt,encode_range:Ot,decode_col:Hl,decode_row:Wl,split_cell:Nv,decode_cell:zt,decode_range:Ir,format_cell:yn,sheet_add_aoa:Af,sheet_add_json:Ch,sheet_add_dom:Sh,aoa_to_sheet:Ls,json_to_sheet:mE,table_to_sheet:Th,table_to_book:Vb,sheet_to_csv:tc,sheet_to_txt:Ih,sheet_to_json:Ui,sheet_to_html:wh,sheet_to_formulae:hE,sheet_to_row_object_array:Ui,sheet_get_cell:Ta,book_new:pE,book_append_sheet:gE,book_set_sheet_visibility:_E,cell_set_number_format:vE,cell_set_hyperlink:Ah,cell_set_internal_link:yE,cell_add_comment:wE,sheet_set_array_formula:SE,consts:{SHEET_VISIBLE:0,SHEET_HIDDEN:1,SHEET_VERY_HIDDEN:2}};const TE=["昵称","手机号","密码"];function bE(){const e=$o.aoa_to_sheet([[...TE],["张三","13800001111","Abc12345"]]);e["!cols"]=[{wch:16},{wch:14},{wch:16}];const t=$o.book_new();$o.book_append_sheet(t,e,"用户导入"),dE(t,"导入用户模板.xlsx")}const EE=[{label:"启用",value:"active"},{label:"封禁",value:"disabled"},{label:"已注销",value:"deleted"}],jE=()=>{const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),n=!!e&&!e.tenant_id,{renderTenantId:s}=Yr(),[i,o]=g.useState(!1),[l,c]=g.useState([]),[d,u]=g.useState(1),[h,m]=g.useState(20),[_,x]=g.useState(0),[f,p]=g.useState({}),[I,S]=g.useState(!1),[w,v]=g.useState(!1),[y,R]=g.useState(null),[j,T]=g.useState(!1),[C,F]=g.useState(!1),[P,V]=g.useState(null),[X,_e]=g.useState(null),[B,te]=g.useState(""),[J,G]=g.useState(!1),[se,Q]=g.useState(!1),[de,k]=g.useState(!1),[W,z]=g.useState(!1),[D,q]=g.useState(!1),[K,Z]=g.useState(!1),[me,ie]=g.useState(!1),[he,we]=g.useState(Y0),[He,ze]=g.useState(!1),[gt,qe]=g.useState(!1),[le,fe]=g.useState(null),[Re,Ue]=g.useState(""),[nt,Tt]=g.useState(!1),[yt,De]=g.useState([]),[Ce,Qe]=g.useState(e==null?void 0:e.tenant_id),[Xe,Je]=g.useState(null),[xe,ke]=g.useState(),[et,_t]=g.useState(!1),[Wt,Sn]=g.useState(!1),[cr,as]=g.useState(null),[Tn]=b.useForm(),[kr]=b.useForm(),[qr]=b.useForm(),[bn]=b.useForm(),[En]=b.useForm(),[St]=b.useForm(),st=async(H=d,ee=h,ye=f)=>{o(!0);try{const Ne=await Lt.list({...ye,page:H,page_size:ee});(Ne.code===0||Ne.code===200)&&Ne.data?(c(Ne.data.items),x(Ne.data.total),u(Ne.data.page),m(Ne.data.size)):E.error(Ne.message||"获取用户列表失败")}catch(Ne){E.error(Ne.message||"获取用户列表失败")}finally{o(!1)}},kt=async H=>{const ee=await R_(H);return we(ee),ee},dr=async H=>{if(Qe(H),!H)return De([]),[];try{const ee=await Fn.list(r?H:void 0);if((ee.code===0||ee.code===200)&&ee.data)return De(ee.data),ee.data;E.error(ee.message||"获取用户分组失败")}catch(ee){E.error(ee.message||"获取用户分组失败")}return[]};g.useEffect(()=>{st(1,h,f),kt(r||e==null?void 0:e.tenant_id),e!=null&&e.tenant_id&&dr(e.tenant_id)},[]);const Qt=H=>{const ee={id:r&&H.id?Number(H.id):void 0,user_no:!r&&H.user_no?Number(H.user_no):void 0,tenant_id:H.tenant_id,policy_group_id:H.policy_group_id,status:H.status,keyword:H.keyword};p(ee),st(1,h,ee)},Ht=async H=>{const ee=H.tenant_id;Ce!==ee&&await dr(ee),Je(H),ke(H.policy_group_id)},ur=async()=>{if(!(!Xe||!xe)){_t(!0);try{const H=await Fn.assignUsers(xe,Xe.tenant_id,[Xe.id]);H.code===0||H.code===200?(E.success("用户分组已调整"),Je(null),st()):E.error(H.message||"调整用户分组失败")}catch(H){E.error(H.message||"调整用户分组失败")}finally{_t(!1)}}},Xr=H=>{const ee=H.status==="active",ye=ee?"封禁":"解封";Me.confirm({title:`${ye}用户`,content:ee?`确认${ye}用户「${H.nickname||H.username}」?${ye}后该用户将被强制下线并无法登录。`:`确认${ye}用户「${H.nickname||H.username}」?${ye}后该用户可以正常登录使用。`,okText:`确认${ye}`,okButtonProps:{danger:ee},cancelText:"取消",onOk:async()=>{try{const Ne=ee?"disabled":"active",We=await Lt.updateStatus(H.id,Ne);We.code===0||We.code===200?(E.success(`用户已${ye}`),st()):E.error(We.message||`${ye}失败`)}catch(Ne){E.error(Ne.message||`${ye}失败`)}}})},It=async()=>{try{const H=await En.validateFields(),ee=Array.from(new Set(String(H.usernames).split(/\r?\n/).map(dt=>dt.trim()).filter(Boolean)));if(ee.length>500){E.error("单次最多封禁 500 个用户");return}q(!0);const ye=await Lt.batchBan(ee);if(!(ye.code===0||ye.code===200)||!ye.data){E.error(ye.message||"批量封禁失败");return}z(!1),En.resetFields(),st();const We=ye.data.results.filter(dt=>dt.status==="failed");Me.info({title:"批量封禁结果",width:640,content:a.jsxs("div",{children:[a.jsxs("div",{style:{marginBottom:We.length>0?12:0},children:["成功 ",ye.data.success_count," 人,失败 ",ye.data.failed_count," 人。 成功项均已撤销登录会话并发送强制下线指令。"]}),We.length>0&&a.jsx(Ye,{size:"small",rowKey:"username",pagination:!1,scroll:{y:300},dataSource:We,columns:[{title:"用户名",dataIndex:"username",width:180},{title:"失败原因",dataIndex:"reason"}]})]})})}catch(H){if(H!=null&&H.errorFields)return;E.error(H.message||"批量封禁失败")}finally{q(!1)}},Fr=async()=>{var H;try{const ee=await St.validateFields();ie(!0);const ye=await Lt.banByIP(String(ee.ip).trim());if(!(ye.code===0||ye.code===200)||!ye.data){E.error(ye.message||"按 IP 封禁失败");return}Z(!1),St.resetFields(),st();const We=ye.data.results.filter(dt=>dt.status==="failed");Me.info({title:"IP 封禁结果",width:640,content:a.jsxs("div",{children:[a.jsxs("div",{style:{marginBottom:We.length>0?12:0},children:["IP ",ye.data.ip,",命中 ",ye.data.matched_count," 人, 成功 ",ye.data.success_count," 人,失败 ",ye.data.failed_count," 人。 成功项均已撤销登录会话并发送强制下线指令。"]}),We.length>0&&a.jsx(Ye,{size:"small",rowKey:"username",pagination:!1,scroll:{y:300},dataSource:We,columns:[{title:"用户名",dataIndex:"username",width:180},{title:"失败原因",dataIndex:"reason"}]})]})})}catch(ee){if(ee!=null&&ee.errorFields)return;if((H=ee==null?void 0:ee.message)!=null&&H.includes("timeout")){E.warning("请求超时,后台可能仍在处理,请稍后刷新用户列表确认状态");return}E.error(ee.message||"按 IP 封禁失败")}finally{ie(!1)}},er=async()=>{kr.resetFields();const H=await kt(r||e==null?void 0:e.tenant_id);kr.setFieldsValue({password:Za(H.minLength)}),T(!0)},vr=async()=>{var H,ee;try{const ye=await kr.validateFields(),Ne=r?ye.tenant_id:e==null?void 0:e.tenant_id;if(!Ne){console.error("租户信息调试:",{admin:e,tenantId:e==null?void 0:e.tenant_id,isPlatformAdmin:r}),E.error(`无法获取租户信息(当前 admin.tenant_id = ${e==null?void 0:e.tenant_id}),请联系管理员`);return}let We=((H=ye.nickname)==null?void 0:H.trim())||void 0;if(!We)try{const{prefixes:bt,suffixes:tr}=await ae();if(bt.length>0&&tr.length>0){const wt=bt[Math.floor(Math.random()*bt.length)],is=tr[Math.floor(Math.random()*tr.length)];We=`${wt}的${is}`}}catch{}const dt=await Lt.create({tenant_id:Ne,phone:((ee=ye.phone)==null?void 0:ee.trim())||void 0,nickname:We,password:ye.password});dt.code===0||dt.code===200?(E.success("用户已创建"),T(!1),st()):E.error(dt.message||"创建用户失败")}catch(ye){if(ye!=null&&ye.errorFields)return;E.error(ye.message||"创建用户失败")}},on=()=>Za(he.minLength),ln=async H=>{V(H),qr.resetFields();const ee=await kt(r?H.tenant_id:e==null?void 0:e.tenant_id);qr.setFieldsValue({password:Za(ee.minLength)}),F(!0)},A=async H=>{const ee=H.getFieldValue("password");if(!ee){E.warning("请先生成密码");return}try{await navigator.clipboard.writeText(ee),E.success("密码已复制到剪贴板")}catch{E.error("复制失败,请手动复制")}},L=async()=>{if(P)try{const H=await qr.validateFields(),ee=await Lt.resetPassword(P.id,H.password);ee.code===0||ee.code===200?(E.success("密码已重置"),F(!1),st()):E.error(ee.message||"重置密码失败")}catch(H){if(H!=null&&H.errorFields)return;E.error(H.message||"重置密码失败")}},O=H=>{const ye=!(H.can_view_security_info??!1);Me.confirm({title:"安全信息查看权限",content:`确认将用户「${H.nickname||H.username}」的安全信息查看权限${ye?"开启":"关闭"}?`,okText:"确认",cancelText:"取消",onOk:async()=>{try{const Ne=await Lt.updateSecurityPerm(H.id,ye);Ne.code===0||Ne.code===200?(E.success("安全权限已更新"),st()):E.error(Ne.message||"更新安全权限失败")}catch(Ne){E.error(Ne.message||"更新安全权限失败")}}})},N=H=>{Me.confirm({title:"强制用户下线",content:`确认强制用户「${H.nickname||H.username}」下线?将踢出该用户的所有在线终端。`,okText:"确认下线",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const ee=await Lt.kick(H.id);ee.code===0||ee.code===200?E.success("用户已强制下线"):E.error(ee.message||"操作失败")}catch(ee){E.error(ee.message||"操作失败")}}})},M=H=>{if(!H.username_editable||!H.username_prefix){E.warning("该账号不符合自动分配规则,不能修改用户名");return}_e(H),te(H.username_suffix||"")},U=async()=>{if(!X||!/^[0-9]{4,8}$/.test(B)){E.error("请输入4至8位数字");return}if(B===X.username_suffix){E.warning("用户名数字部分没有变化");return}G(!0);try{const H=await Lt.updateUsername(X.id,B);if(!(H.code===0||H.code===200)||!H.data){E.error(H.message||"修改用户名失败");return}const ye=H.data;ye.force_logout_succeeded?E.success("用户名已修改,用户已被强制下线"):E.warning("用户名已修改,但部分会话下线失败,请手动执行强制下线"),c(Ne=>Ne.map(We=>We.id===X.id?{...We,username:ye.username,username_prefix:ye.username_prefix,username_suffix:ye.username_suffix}:We)),R(Ne=>(Ne==null?void 0:Ne.id)===X.id?{...Ne,username:ye.username,username_prefix:ye.username_prefix,username_suffix:ye.username_suffix}:Ne),_e(null)}catch(H){E.error(H.message||"修改用户名失败")}finally{G(!1)}},ne=async H=>{if(fe(H),H.login_ip_whitelist!==void 0)Ue(H.login_ip_whitelist);else try{const ee=await Lt.detail(H.id),ye=ee.code===0||ee.code===200;Ue(ye&&ee.data?ee.data.login_ip_whitelist??"":"")}catch{Ue("")}qe(!0)},ge=async()=>{if(le){Tt(!0);try{const H=await Lt.updateIPWhitelist(le.id,Re.trim());H.code===0||H.code===200?(E.success("IP 白名单已保存"),qe(!1),st()):E.error(H.message||"保存失败")}catch(H){E.error(H.message||"保存失败")}finally{Tt(!1)}}},re=H=>{Me.confirm({title:"强制注销用户",content:`确认强制注销用户「${H.nickname||H.username}」?注销后该用户数据将被清除且无法恢复。`,okText:"确认注销",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const ee=await Lt.forceDeactivate(H.id);ee.code===0||ee.code===200?(E.success("用户已强制注销"),st()):E.error(ee.message||"注销失败")}catch(ee){E.error(ee.message||"注销失败")}}})},ae=async()=>{const[H,ee]=await Promise.all([fetch("/前缀.txt").then(We=>We.text()),fetch("/后缀.txt").then(We=>We.text())]),ye=H.split(` +`).map(We=>We.trim()).filter(Boolean),Ne=ee.split(` +`).map(We=>We.trim()).filter(Boolean);return{prefixes:ye,suffixes:Ne}},oe=(H,ee,ye)=>{const Ne=[];for(let We=0;We{const Ne=new Date,We=`${Ne.getFullYear()}-${String(Ne.getMonth()+1).padStart(2,"0")}-${String(Ne.getDate()).padStart(2,"0")} ${String(Ne.getHours()).padStart(2,"0")}:${String(Ne.getMinutes()).padStart(2,"0")}:${String(Ne.getSeconds()).padStart(2,"0")}`;let dt=`批量创建用户 - 生成时间:${We} +`;r&&(dt+=`租户 ID:${ye} +`),dt+=`创建数量:${H.length} +`,dt+=`================================================ +`,dt+=`昵称 用户名 密码 +`,dt+=`------------------------------------------------ +`;for(const wt of H){const is=ee.get(wt.nickname)||"";dt+=`${wt.nickname} ${wt.username} ${is} +`}const yr=new Blob(["\uFEFF"+dt],{type:"text/plain;charset=utf-8"}),bt=URL.createObjectURL(yr),tr=document.createElement("a");tr.href=bt,tr.download=`批量创建用户_${We.replace(/[: ]/g,"-")}.txt`,document.body.appendChild(tr),tr.click(),document.body.removeChild(tr),URL.revokeObjectURL(bt)},Pe=async()=>{try{const H=await bn.validateFields(),ee=r?H.tenant_id:e==null?void 0:e.tenant_id;if(!ee){E.error("无法获取租户信息");return}const ye=H.count;k(!0);let Ne,We;try{const wt=await ae();Ne=wt.prefixes,We=wt.suffixes}catch{E.error("加载前缀/后缀文件失败"),k(!1);return}if(Ne.length===0||We.length===0){E.error("前缀或后缀文件为空"),k(!1);return}const dt=oe(ye,Ne,We),yr=new Map;dt.forEach(wt=>yr.set(wt.nickname,wt.password));const bt=await Lt.batchCreate({tenant_id:ee,users:dt});if((bt.code===0||bt.code===200)&&bt.data){const{created:wt,failed:is}=bt.data;E.success(`成功创建 ${wt.length} 名用户${is>0?`,${is} 名失败`:""}`),Ee(wt,yr,ee),Q(!1),st()}else E.error(bt.message||"批量创建失败")}catch(H){if(H!=null&&H.errorFields)return;E.error(H.message||"批量创建失败")}finally{k(!1)}},Be=async H=>{let ee;if(r){if(ee=Tn.getFieldValue("tenant_id"),!ee){E.warning("平台管理员请先在筛选栏选择租户,再进行 Excel 导入");return}}else if(ee=e==null?void 0:e.tenant_id,!ee){E.error("无法获取租户信息");return}const ye=new FormData;ye.append("file",H),ye.append("tenant_id",String(ee)),ze(!0);try{const Ne=await Lt.importExcel(ye);(Ne.code===0||Ne.code===200)&&Ne.data?(as(Ne.data),Sn(!0),st()):E.error(Ne.message||"导入失败")}catch(Ne){E.error(Ne.message||"导入失败")}finally{ze(!1)}},Ae=async H=>{S(!0),v(!0);try{const ee=await Lt.detail(H);(ee.code===0||ee.code===200)&&ee.data?R(ee.data):E.error(ee.message||"获取用户详情失败")}catch(ee){E.error(ee.message||"获取用户详情失败")}finally{v(!1)}},Le=[...r?[{title:"ID",dataIndex:"id",width:80}]:[],{title:"编号",dataIndex:"user_no",width:80},{title:"头像",dataIndex:"avatar",width:80,render:(H,ee)=>{const ye=ee.status==="deleted",Ne=ye&&ee.deactivated_avatar||H,We=ye?ee.deactivated_nickname||ee.nickname||ee.username:ee.nickname||ee.username;return Ne?a.jsx(Mr,{src:Ne,alt:We,width:40,height:40,style:{objectFit:"cover",cursor:"pointer",opacity:ye?.5:1},preview:{mask:"点击查看"}}):a.jsx(da,{size:40,shape:"square",icon:a.jsx(ca,{}),style:ye?{opacity:.5}:void 0,children:We?We.charAt(0):""})}},{title:"用户名",dataIndex:"username",width:160},{title:"昵称",dataIndex:"nickname",width:160,render:(H,ee)=>ee.status==="deleted"&&ee.deactivated_nickname?a.jsx("span",{style:{color:"#999"},children:ee.deactivated_nickname}):ee.nickname||"--"},...r?[{title:"租户",dataIndex:"tenant_id",width:160,render:s}]:[],{title:"手机号",dataIndex:"phone",width:140},{title:"注册方式",dataIndex:"register_source",width:100,render:H=>({admin:"后台创建",ios:"iOS 注册",android:"安卓注册"})[H]||H||"--"},{title:"状态",dataIndex:"status",width:100,render:H=>{const ee={active:{color:"green",text:"正常"},disabled:{color:"red",text:"已封禁"},deleted:{color:"default",text:"已注销"}},ye=ee[H]||ee.disabled;return a.jsx(Te,{color:ye.color,children:ye.text})}},{title:"用户分组",dataIndex:"policy_group_name",width:140,render:H=>H||"--"},{title:"最近登录",dataIndex:"last_login_at",width:180,render:H=>H?Ke(H):"--"},{title:"创建时间",dataIndex:"created_at",width:180,render:H=>Ke(H)},{title:"操作",key:"actions",width:400,render:(H,ee)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>Ae(ee.id),children:"详情"}),ee.status!=="deleted"&&a.jsxs(a.Fragment,{children:[a.jsx(Y,{size:"small",onClick:()=>ln(ee),children:"重置密码"}),a.jsx(Y,{size:"small",onClick:()=>ne(ee),children:"IP白名单"}),a.jsx(Y,{size:"small",onClick:()=>Ht(ee),children:"调整分组"}),(n||ee.tenant_enable_security_perm)&&a.jsx(Y,{size:"small",type:ee.can_view_security_info??!1?"primary":"default",onClick:()=>O(ee),children:ee.can_view_security_info??!1?"关闭安全权限":"开启安全权限"}),a.jsx(Y,{size:"small",danger:ee.status==="active",onClick:()=>Xr(ee),children:ee.status==="active"?"封禁":"解封"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>N(ee),children:"强制下线"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>re(ee),children:"强制注销"})]})]})}];return a.jsxs("div",{className:"admin-users",children:[a.jsx(be,{className:"admin-users-filter",children:a.jsxs(b,{form:Tn,layout:"inline",onFinish:Qt,children:[r?a.jsx(b.Item,{label:"ID",name:"id",children:a.jsx(ve,{placeholder:"用户 ID",allowClear:!0,style:{width:120}})}):a.jsx(b.Item,{label:"编号",name:"user_no",children:a.jsx(ve,{placeholder:"用户编号",allowClear:!0,style:{width:120}})}),r&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200},onChange:H=>{Tn.setFieldValue("policy_group_id",void 0),dr(H)}})}),a.jsx(b.Item,{label:"用户分组",name:"policy_group_id",children:a.jsx($e,{placeholder:r&&!Ce?"请先选择租户":"全部",allowClear:!0,disabled:r&&!Ce,options:yt.map(H=>({label:H.name,value:H.id})),style:{width:150}})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:EE,style:{width:120}})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"用户名/昵称/手机号",allowClear:!0})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>Tn.resetFields(),children:"重置"}),a.jsx(Y,{onClick:er,children:"创建用户"}),a.jsx(Y,{onClick:()=>{bn.resetFields(),Q(!0)},children:"批量创建"}),r&&a.jsxs(a.Fragment,{children:[a.jsx(Y,{danger:!0,icon:a.jsx(ju,{}),onClick:()=>{En.resetFields(),z(!0)},children:"批量封禁"}),a.jsx(Y,{danger:!0,icon:a.jsx(Yo,{}),onClick:()=>{St.resetFields(),Z(!0)},children:"按 IP 封禁"})]}),a.jsx(Y,{icon:a.jsx(Iu,{}),loading:He,onClick:()=>{const H=document.createElement("input");H.type="file",H.accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",H.onchange=ee=>{var Ne;const ye=(Ne=ee.target.files)==null?void 0:Ne[0];ye&&Be(ye)},H.click()},children:"导入Excel"}),a.jsx(Y,{icon:a.jsx($r,{}),onClick:bE,children:"下载模板"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:Le,dataSource:l,loading:i,scroll:{x:900},pagination:{current:d,pageSize:h,total:_,showSizeChanger:!0,onChange:(H,ee)=>{u(H),m(ee),st(H,ee)}}})}),a.jsx($i,{title:"用户详情",width:420,open:I,onClose:()=>S(!1),children:a.jsx(Mn,{spinning:w,children:y&&a.jsxs(a.Fragment,{children:[a.jsx("div",{style:{textAlign:"center",marginBottom:16},children:(()=>{const H=y.status==="deleted",ee=H&&y.deactivated_avatar||y.avatar,ye=H?y.deactivated_nickname||y.nickname||y.username:y.nickname||y.username;return ee?a.jsx(Mr,{src:ee,alt:ye,width:80,height:80,style:{objectFit:"cover",cursor:"pointer",opacity:H?.5:1},preview:{mask:"点击查看大图"}}):a.jsx(da,{size:80,shape:"square",icon:a.jsx(ca,{}),style:H?{opacity:.5}:void 0,children:ye?ye.charAt(0):""})})()}),a.jsxs(je,{column:1,bordered:!0,size:"small",children:[r&&a.jsx(je.Item,{label:"ID",children:y.id}),a.jsx(je.Item,{label:"用户名",children:a.jsxs(Ie,{children:[a.jsx("span",{children:y.username}),y.status!=="deleted"&&y.username_editable&&a.jsx(Y,{size:"small",onClick:()=>M(y),children:"修改"})]})}),a.jsx(je.Item,{label:"昵称",children:y.nickname||"--"}),y.status==="deleted"&&y.deactivated_nickname&&a.jsx(je.Item,{label:"注销前昵称",children:a.jsx("span",{style:{color:"#fa8c16"},children:y.deactivated_nickname})}),r&&a.jsx(je.Item,{label:"租户",children:s(y.tenant_id)}),a.jsx(je.Item,{label:"手机号",children:y.phone||"--"}),a.jsx(je.Item,{label:"注册方式",children:y.register_source==="admin"?"后台创建":y.register_source==="ios"?"iOS 注册":y.register_source==="android"?"安卓注册":y.register_source||"--"}),y.register_source!=="admin"&&y.register_ip&&a.jsx(je.Item,{label:"注册IP",children:y.register_ip}),a.jsx(je.Item,{label:"状态",children:a.jsx(Te,{color:y.status==="active"?"green":y.status==="deleted"?"default":"red",children:y.status==="active"?"正常":y.status==="deleted"?"已注销":"已封禁"})}),a.jsx(je.Item,{label:"用户分组",children:y.policy_group_name||"--"}),y.deactivated_at&&a.jsx(je.Item,{label:"注销时间",children:Ke(y.deactivated_at)}),y.last_login_at&&a.jsx(je.Item,{label:"最近登录时间",children:Ke(y.last_login_at)}),y.last_login_ip&&a.jsx(je.Item,{label:"最近登录IP",children:y.last_login_ip}),a.jsx(je.Item,{label:"创建时间",children:Ke(y.created_at)}),a.jsx(je.Item,{label:"更新时间",children:Ke(y.updated_at)})]})]})})}),a.jsxs(Me,{title:"修改用户名",open:!!X,onCancel:()=>_e(null),onOk:U,okText:"确认修改",cancelText:"取消",confirmLoading:J,destroyOnClose:!0,children:[a.jsx("div",{style:{marginBottom:8,color:"#666"},children:"只能修改前缀后的数字,修改后该用户所有设备会被强制下线。"}),a.jsx(ve,{addonBefore:X==null?void 0:X.username_prefix,value:B,maxLength:8,placeholder:"请输入4至8位数字",onChange:H=>te(H.target.value.replace(/\D/g,"").slice(0,8))}),a.jsxs("div",{style:{marginTop:8,color:"#888",fontSize:12},children:["新用户名:",(X==null?void 0:X.username_prefix)||"",B]})]}),a.jsx(Me,{title:`调整分组${Xe?`:${Xe.nickname||Xe.username}`:""}`,open:!!Xe,onCancel:()=>Je(null),onOk:ur,okText:"保存",cancelText:"取消",confirmLoading:et,children:a.jsx($e,{value:xe,onChange:ke,options:yt.map(H=>({label:`${H.name}${H.enabled?"":"(已停用)"}`,value:H.id,disabled:!H.enabled})),style:{width:"100%"},placeholder:"请选择用户分组"})}),a.jsx(Me,{title:"创建用户",open:j,onCancel:()=>T(!1),onOk:vr,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:kr,layout:"vertical",children:[r&&a.jsx(b.Item,{label:"租户",name:"tenant_id",rules:[{required:!0,message:"请选择租户"}],children:a.jsx(ot,{style:{width:"100%"},onChange:H=>{kt(H).then(ee=>{kr.setFieldsValue({password:Za(ee.minLength)})})}})}),a.jsx(b.Item,{label:"手机号",name:"phone",children:a.jsx(ve,{placeholder:"可选手机号"})}),a.jsx(b.Item,{label:"昵称",name:"nickname",children:a.jsx(ve,{placeholder:"用户昵称"})}),a.jsx(b.Item,{label:"密码",name:"password",tooltip:od(he),rules:ld(he),children:a.jsx(ve,{placeholder:"自动生成,可手动修改",addonAfter:a.jsx(Y,{type:"text",icon:a.jsx(un,{}),onClick:()=>A(kr),size:"small",style:{padding:"0 4px"},children:"复制"})})})]})}),a.jsx(Me,{title:`重置密码${P?`:${P.nickname||P.username}`:""}`,open:C,onCancel:()=>F(!1),onOk:L,okText:"确认",cancelText:"取消",destroyOnClose:!0,children:a.jsx(b,{form:qr,layout:"vertical",children:a.jsx(b.Item,{label:"新密码",name:"password",tooltip:od(he),rules:ld(he,"请输入新密码"),children:a.jsx(ve,{placeholder:"自动生成,可手动修改",addonAfter:a.jsx(Y,{type:"text",icon:a.jsx(un,{}),onClick:()=>A(qr),size:"small",style:{padding:"0 4px"},children:"复制"})})})})}),a.jsx(Me,{title:"批量创建用户",open:se,onCancel:()=>Q(!1),onOk:Pe,okText:"创建",cancelText:"取消",confirmLoading:de,destroyOnClose:!0,children:a.jsxs(b,{form:bn,layout:"vertical",children:[r&&a.jsx(b.Item,{label:"租户",name:"tenant_id",rules:[{required:!0,message:"请选择租户"}],children:a.jsx(ot,{style:{width:"100%"}})}),a.jsx(b.Item,{label:"创建数量",name:"count",rules:[{required:!0,message:"请输入创建数量"}],initialValue:10,children:a.jsx(at,{min:1,max:100,style:{width:"100%"}})}),a.jsxs("div",{style:{color:"#999",fontSize:12},children:["昵称从前缀/后缀词库随机组合,密码自动生成 12 位随机字符。",a.jsx("br",{}),"创建完成后自动下载包含用户名和密码的 TXT 文件。"]})]})}),a.jsx(Me,{title:"批量封禁用户",open:W,onCancel:()=>z(!1),onOk:It,okText:"确认封禁",okButtonProps:{danger:!0},cancelText:"取消",confirmLoading:D,destroyOnClose:!0,children:a.jsx(b,{form:En,layout:"vertical",children:a.jsx(b.Item,{label:"用户名",name:"usernames",rules:[{required:!0,message:"请输入至少一个用户名"}],extra:"每行一个用户名,自动忽略空行和重复项,单次最多 500 个。封禁成功后会立即撤销全部登录会话并踢下线。",children:a.jsx(ve.TextArea,{rows:12,placeholder:`user0001 +user0002 +user0003`})})})}),a.jsx(Me,{title:"按 IP 封禁用户",open:K,onCancel:()=>Z(!1),onOk:Fr,okText:"确认封禁并下线",okButtonProps:{danger:!0},cancelText:"取消",confirmLoading:me,destroyOnClose:!0,children:a.jsx(b,{form:St,layout:"vertical",children:a.jsx(b.Item,{label:"IP 地址",name:"ip",rules:[{required:!0,message:"请输入 IPv4 或 IPv6 地址"}],extra:"匹配全部租户账号的注册 IP、最近登录 IP 和历史登录 IP。命中的全部非注销账号都会被封禁,并立即撤销登录会话、强制下线。",children:a.jsx(ve,{placeholder:"例如 203.0.113.10 或 2001:db8::1",allowClear:!0})})})}),a.jsx(Me,{title:"Excel 导入结果",open:Wt,onCancel:()=>Sn(!1),footer:null,width:600,destroyOnClose:!0,children:cr&&a.jsxs("div",{children:[a.jsxs("div",{style:{marginBottom:16,fontSize:15},children:[a.jsxs("span",{style:{color:"#52c41a",fontWeight:600},children:["成功 ",cr.success_count," 人"]}),a.jsx("span",{style:{margin:"0 12px"},children:"|"}),a.jsxs("span",{style:{color:cr.failed_count>0?"#ff4d4f":"#999",fontWeight:600},children:["失败 ",cr.failed_count," 人"]})]}),cr.failed_items&&cr.failed_items.length>0&&a.jsxs(a.Fragment,{children:[a.jsx("div",{style:{marginBottom:8,fontWeight:500},children:"失败详情:"}),a.jsx(Ye,{size:"small",rowKey:"row",dataSource:cr.failed_items,pagination:!1,scroll:{y:300},columns:[{title:"行号",dataIndex:"row",width:60},{title:"昵称",dataIndex:"nickname",width:100,render:H=>H||"--"},{title:"失败原因",dataIndex:"reason"}]})]})]})}),a.jsxs(Me,{title:`IP 白名单${le?`:${le.nickname||le.username}`:""}`,open:gt,onCancel:()=>qe(!1),onOk:ge,okText:"保存",cancelText:"取消",confirmLoading:nt,destroyOnClose:!0,children:[a.jsx("div",{style:{marginBottom:8,color:"#666",fontSize:13},children:"填写允许登录的 IPv4、IPv6 或 CIDR,多个规则用逗号或换行分隔。留空表示不限制登录 IP。"}),a.jsx(ve.TextArea,{value:Re,onChange:H=>Ue(H.target.value),placeholder:`例:203.0.113.10 +2001:db8::/32`,autoSize:{minRows:3,maxRows:6},allowClear:!0}),Re.trim()&&a.jsxs("div",{style:{marginTop:8,color:"#888",fontSize:12},children:["当前限制 IP:",Re.split(",").map(H=>H.trim()).filter(Boolean).map(H=>a.jsx(Te,{style:{marginLeft:4},children:H},H))]})]})]})},{TextArea:IE}=ve,CE=e=>e<1024?e+" B":e<1024*1024?(e/1024).toFixed(1)+" KB":e<1024*1024*1024?(e/(1024*1024)).toFixed(1)+" MB":(e/(1024*1024*1024)).toFixed(1)+" GB",qd={xlsx:{icon:vs,color:"#52c41a"},xls:{icon:vs,color:"#52c41a"},csv:{icon:vs,color:"#52c41a"},doc:{icon:_i,color:"#1890ff"},docx:{icon:_i,color:"#1890ff"},ppt:{icon:gi,color:"#fa8c16"},pptx:{icon:gi,color:"#fa8c16"},mp4:{icon:An,color:"#1890ff"},avi:{icon:An,color:"#1890ff"},mov:{icon:An,color:"#1890ff"},mkv:{icon:An,color:"#1890ff"},wmv:{icon:An,color:"#1890ff"},flv:{icon:An,color:"#1890ff"},webm:{icon:An,color:"#1890ff"},mp3:{icon:zn,color:"#722ed1"},wav:{icon:zn,color:"#722ed1"},flac:{icon:zn,color:"#722ed1"},aac:{icon:zn,color:"#722ed1"},ogg:{icon:zn,color:"#722ed1"},wma:{icon:zn,color:"#722ed1"},jpg:{icon:Cn,color:"#52c41a"},jpeg:{icon:Cn,color:"#52c41a"},png:{icon:Cn,color:"#52c41a"},gif:{icon:Cn,color:"#52c41a"},bmp:{icon:Cn,color:"#52c41a"},webp:{icon:Cn,color:"#52c41a"},svg:{icon:Cn,color:"#52c41a"},txt:{icon:Xs,color:"#8c8c8c"},log:{icon:Xs,color:"#8c8c8c"},zip:{icon:rn,color:"#faad14"},rar:{icon:rn,color:"#faad14"},"7z":{icon:rn,color:"#faad14"},tar:{icon:rn,color:"#faad14"},gz:{icon:rn,color:"#faad14"},md:{icon:Cu,color:"#595959"},js:{icon:rr,color:"#fadb14"},ts:{icon:rr,color:"#1890ff"},jsx:{icon:rr,color:"#fadb14"},tsx:{icon:rr,color:"#1890ff"},py:{icon:rr,color:"#389e0d"},go:{icon:rr,color:"#13c2c2"},java:{icon:rr,color:"#f5222d"},html:{icon:rr,color:"#fa541c"},css:{icon:rr,color:"#1890ff"},json:{icon:rr,color:"#8c8c8c"},pdf:{icon:pl,color:"#f5222d"}},AE=(e,t)=>{var r;if(t){const n=(r=t.split(".").pop())==null?void 0:r.toLowerCase();if(n&&qd[n]){const{icon:s,color:i}=qd[n];return a.jsx(s,{style:{fontSize:18,color:i}})}}return e!=null&&e.startsWith("image/")?a.jsx(Cn,{style:{fontSize:18,color:"#52c41a"}}):e!=null&&e.startsWith("video/")?a.jsx(An,{style:{fontSize:18,color:"#1890ff"}}):e!=null&&e.startsWith("audio/")?a.jsx(zn,{style:{fontSize:18,color:"#722ed1"}}):e==="application/pdf"?a.jsx(pl,{style:{fontSize:18,color:"#f5222d"}}):a.jsx(Au,{style:{fontSize:18}})},oi={normal:{label:"正常",color:"green"},banned:{label:"封禁",color:"red"},dissolved:{label:"已解散",color:"default"}},kE=()=>{var Ue,nt,Tt,yt;const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),{renderTenantId:n}=Yr(),[s,i]=g.useState(!1),[o,l]=g.useState([]),[c,d]=g.useState(1),[u,h]=g.useState(20),[m,_]=g.useState(0),[x,f]=g.useState({}),[p,I]=g.useState(!1),[S,w]=g.useState(!1),[v,y]=g.useState(null),[R,j]=g.useState(!1),[T,C]=g.useState(null),[F]=b.useForm(),[P]=b.useForm(),[V,X]=g.useState(null),[_e,B]=g.useState(!1),[te,J]=g.useState([]),[G,se]=g.useState(1),[Q]=g.useState(20),[de,k]=g.useState(0),[W,z]=g.useState(""),[D,q]=g.useState("all"),[K,Z]=g.useState(null),me=g.useCallback(async(De,Ce=1,Qe="",Xe="all")=>{B(!0);try{const Je=await ad.list(De,{keyword:Qe||void 0,type:Xe==="all"?void 0:Xe,page:Ce,page_size:Q});(Je.code===0||Je.code===200)&&Je.data?(J(Je.data.items),k(Je.data.total),se(Je.data.page)):E.error(Je.message||"获取群文件失败")}catch(Je){E.error(Je.message||"获取群文件失败")}finally{B(!1)}},[Q]),ie=De=>{X(De),z(""),q("all"),J([]),k(0),se(1),me(De.id)},he=(De,Ce)=>{Me.confirm({title:"确认删除该文件?",content:`文件名:${Ce.file_name}`,okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const Qe=await ad.delete(De,Ce.id);Qe.code===0||Qe.code===200?(E.success("文件已删除"),J(Je=>Je.filter(xe=>xe.id!==Ce.id)),k(Je=>Math.max(0,Je-1))):E.error(Qe.message||"删除失败")}catch(Qe){E.error(Qe.message||"删除失败")}}})},we=async(De=c,Ce=u,Qe=x)=>{i(!0);try{const Xe=await Wn.list({...Qe,page:De,page_size:Ce});(Xe.code===0||Xe.code===200)&&Xe.data?(l(Xe.data.items),_(Xe.data.total),d(Xe.data.page),h(Xe.data.size)):E.error(Xe.message||"获取群组列表失败")}catch(Xe){E.error(Xe.message||"获取群组列表失败")}finally{i(!1)}};g.useEffect(()=>{we(1,u,x)},[]);const He=De=>{const Ce={keyword:De.keyword,status:De.status||void 0};r?(Ce.tenant_id=De.tenant_id,Ce.id=De.id?Number(De.id):void 0):Ce.group_code=De.group_code||void 0,f(Ce),we(1,u,Ce)},ze=De=>{Me.confirm({title:"确认解散该群组?",content:"解散群组会清理成员与会话数据,群记录和消息记录将保留。",okText:"解散",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const Ce=await Wn.dismiss(De.id);Ce.code===0||Ce.code===200?(E.success("群组已解散"),we()):E.error(Ce.message||"解散失败")}catch(Ce){E.error(Ce.message||"解散失败")}}})},gt=De=>{C(De),j(!0),P.resetFields()},qe=async()=>{if(T)try{const De=await P.validateFields(),Ce=await Wn.ban(T.id,De.reason);Ce.code===0||Ce.code===200?(E.success("群组已封禁"),j(!1),we()):E.error(Ce.message||"封禁失败")}catch(De){De.message&&E.error(De.message||"封禁失败")}},le=De=>{Me.confirm({title:"确认解封该群组?",content:"解封后群成员可正常使用群聊功能。",okText:"解封",cancelText:"取消",onOk:async()=>{try{const Ce=await Wn.unban(De.id);Ce.code===0||Ce.code===200?(E.success("群组已解封"),we()):E.error(Ce.message||"解封失败")}catch(Ce){E.error(Ce.message||"解封失败")}}})},fe=async De=>{I(!0),w(!0);try{const Ce=await Wn.detail(De);(Ce.code===0||Ce.code===200)&&Ce.data?y(Ce.data):E.error(Ce.message||"获取群组详情失败")}catch(Ce){E.error(Ce.message||"获取群组详情失败")}finally{w(!1)}},Re=[r?{title:"ID",dataIndex:"id",width:80}:{title:"群组编号",dataIndex:"group_no",width:100},{title:"群号",dataIndex:"group_code",width:120,render:De=>De||"--"},{title:"群组名称",dataIndex:"name",width:220,render:(De,Ce)=>a.jsxs(Ie,{children:[a.jsx(da,{src:Ce.avatar||void 0,icon:a.jsx($n,{}),size:"small",shape:"square",style:{cursor:Ce.avatar?"pointer":void 0},onClick:Qe=>{Ce.avatar&&(Qe==null||Qe.stopPropagation(),Me.info({title:Ce.name,icon:null,content:a.jsx("div",{style:{textAlign:"center",padding:"16px 0"},children:a.jsx("img",{src:Ce.avatar,alt:Ce.name,style:{maxWidth:"100%",maxHeight:400,borderRadius:4}})}),okText:"关闭",width:480}))}}),Ce.name]})},...r?[{title:"租户",dataIndex:"tenant_id",width:160,render:n}]:[],{title:"成员数",dataIndex:"member_count",width:100},{title:"群主昵称",dataIndex:"owner",width:140,render:(De,Ce)=>{var Qe,Xe;return((Qe=Ce.owner)==null?void 0:Qe.nickname)||((Xe=Ce.owner)==null?void 0:Xe.username)||"--"}},r?{title:"群主用户 ID",dataIndex:"owner_id",width:100}:{title:"群主用户名",dataIndex:["owner","username"],width:100,render:De=>De||"--"},{title:"状态",dataIndex:"status",width:90,render:De=>{const Ce=oi[De]||oi.normal;return a.jsx(Te,{color:Ce.color,children:Ce.label})}},{title:"创建时间",dataIndex:"created_at",width:180,render:De=>Ke(De)},{title:"操作",key:"actions",width:280,render:(De,Ce)=>{const Qe=Ce.status||"normal";return a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>fe(Ce.id),children:"详情"}),a.jsx(Y,{size:"small",icon:a.jsx(Eu,{}),onClick:()=>ie(Ce),children:"群文件"}),Qe==="normal"&&a.jsxs(a.Fragment,{children:[a.jsx(Y,{size:"small",danger:!0,onClick:()=>gt(Ce),children:"封禁"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>ze(Ce),children:"解散"})]}),Qe==="banned"&&a.jsx(Y,{size:"small",onClick:()=>le(Ce),children:"解封"})]})}}];return a.jsxs("div",{className:"admin-groups",children:[a.jsx(be,{className:"admin-groups-filter",children:a.jsxs(b,{form:F,layout:"inline",onFinish:He,children:[r&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),r?a.jsx(b.Item,{label:"ID",name:"id",children:a.jsx(ve,{placeholder:"群组 ID",allowClear:!0})}):a.jsx(b.Item,{label:"群号",name:"group_code",children:a.jsx(ve,{placeholder:"群号",allowClear:!0})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"群名称",allowClear:!0})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsxs($e,{placeholder:"全部",allowClear:!0,style:{width:100},children:[a.jsx($e.Option,{value:"normal",children:"正常"}),a.jsx($e.Option,{value:"banned",children:"封禁"}),a.jsx($e.Option,{value:"dissolved",children:"已解散"})]})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>F.resetFields(),children:"重置"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:Re,dataSource:o,loading:s,scroll:{x:1e3},pagination:{current:c,pageSize:u,total:m,showSizeChanger:!0,onChange:(De,Ce)=>{d(De),h(Ce),we(De,Ce)}}})}),a.jsx($i,{title:"群组详情",width:420,open:p,onClose:()=>I(!1),children:a.jsx(Mn,{spinning:S,children:v&&a.jsxs(je,{column:1,bordered:!0,size:"small",children:[r?a.jsx(je.Item,{label:"ID",children:v.id}):a.jsx(je.Item,{label:"群组编号",children:v.group_no||"--"}),a.jsx(je.Item,{label:"群号",children:v.group_code||"--"}),a.jsx(je.Item,{label:"群头像",children:a.jsx(da,{src:v.avatar||void 0,icon:a.jsx($n,{}),size:48,shape:"square",style:{cursor:v.avatar?"pointer":void 0},onClick:()=>{v.avatar&&Me.info({title:v.name,icon:null,content:a.jsx("div",{style:{textAlign:"center",padding:"16px 0"},children:a.jsx("img",{src:v.avatar,alt:v.name,style:{maxWidth:"100%",maxHeight:400,borderRadius:4}})}),okText:"关闭",width:480})}})}),a.jsx(je.Item,{label:"群名称",children:v.name}),r&&a.jsx(je.Item,{label:"租户 ID",children:v.tenant_id}),a.jsx(je.Item,{label:"群主昵称",children:((Ue=v.owner)==null?void 0:Ue.nickname)||((nt=v.owner)==null?void 0:nt.username)||"--"}),a.jsx(je.Item,{label:r?"群主用户 ID":"群主用户名",children:r?((Tt=v.owner)==null?void 0:Tt.id)||v.owner_id:((yt=v.owner)==null?void 0:yt.username)||"--"}),a.jsx(je.Item,{label:"状态",children:(()=>{const De=oi[v.status||"normal"]||oi.normal;return a.jsx(Te,{color:De.color,children:De.label})})()}),v.status==="banned"&&v.ban_reason&&a.jsx(je.Item,{label:"封禁原因",children:v.ban_reason}),a.jsx(je.Item,{label:"成员数",children:v.member_count}),a.jsx(je.Item,{label:"最大人数",children:v.max_members}),a.jsx(je.Item,{label:"群简介",children:v.description||"--"}),a.jsx(je.Item,{label:"创建时间",children:Ke(v.created_at)}),v.status==="dissolved"&&v.dissolved_at&&a.jsx(je.Item,{label:"解散时间",children:Ke(v.dissolved_at)})]})})}),a.jsxs(Me,{title:"封禁群组",open:R,onOk:qe,onCancel:()=>j(!1),okText:"确认封禁",okButtonProps:{danger:!0},cancelText:"取消",children:[a.jsxs("p",{children:["确认封禁群组 ",a.jsx("strong",{children:T==null?void 0:T.name}),"?"]}),a.jsx("p",{style:{color:"#999",fontSize:12},children:"封禁后将向群内发送系统消息,所有成员将无法发送消息和进行群操作。"}),a.jsx(b,{form:P,layout:"vertical",children:a.jsx(b.Item,{label:"封禁原因",name:"reason",rules:[{required:!0,message:"请输入封禁原因"}],children:a.jsx(IE,{rows:3,placeholder:"请输入封禁原因",maxLength:500,showCount:!0})})})]}),a.jsx(Me,{open:!!V,onCancel:()=>X(null),footer:null,centered:!0,destroyOnClose:!0,width:"90%",style:{maxWidth:1100},title:`群文件 - ${(V==null?void 0:V.name)||""}`,children:V&&a.jsxs("div",{children:[a.jsxs("div",{style:{display:"flex",gap:12,marginBottom:16,alignItems:"center",flexWrap:"wrap"},children:[a.jsx(qo,{value:D,options:[{label:"全部",value:"all"},{label:"图片",value:"image"},{label:"视频",value:"video"},{label:"音频",value:"audio"},{label:"文档",value:"document"},{label:"其他",value:"other"}],onChange:De=>{const Ce=De;q(Ce),me(V.id,1,W,Ce)}}),a.jsx(ve,{placeholder:"搜索文件名",prefix:a.jsx(Ar,{}),allowClear:!0,style:{width:200},value:W,onChange:De=>z(De.target.value),onPressEnter:()=>me(V.id,1,W,D)})]}),a.jsx(Ye,{rowKey:"id",dataSource:te,loading:_e,size:"small",scroll:{x:700},pagination:{current:G,pageSize:Q,total:de,showSizeChanger:!1,onChange:De=>me(V.id,De,W,D)},columns:[{title:"文件",dataIndex:"file_name",ellipsis:!0,render:(De,Ce)=>a.jsxs(Ie,{children:[AE(Ce.content_type,Ce.file_name),a.jsx("span",{children:Ce.file_name})]})},{title:"大小",dataIndex:"file_size",width:100,render:De=>CE(De)},{title:"上传者",dataIndex:"uploader",width:140,render:(De,Ce)=>{var Qe,Xe;return((Qe=Ce.uploader)==null?void 0:Qe.nickname)||((Xe=Ce.uploader)==null?void 0:Xe.username)||"--"}},{title:"上传时间",dataIndex:"created_at",width:170,render:De=>Ke(De)},{title:"操作",key:"actions",width:140,render:(De,Ce)=>{var Qe;return a.jsxs(Ie,{children:[((Qe=Ce.content_type)==null?void 0:Qe.startsWith("image/"))&&a.jsx(Xt,{title:"预览",children:a.jsx(Y,{size:"small",type:"text",icon:a.jsx(xl,{}),onClick:()=>Z(Ce.file_url)})}),a.jsx(Xt,{title:"下载",children:a.jsx(Y,{size:"small",type:"text",icon:a.jsx($r,{}),onClick:()=>window.open(Ce.file_url,"_blank")})}),a.jsx(Xt,{title:"删除",children:a.jsx(Y,{size:"small",type:"text",danger:!0,icon:a.jsx(Gs,{}),onClick:()=>he(V.id,Ce)})})]})}}]}),K&&a.jsx(Mr,{src:K,style:{display:"none"},preview:{visible:!0,onVisibleChange:De=>{De||Z(null)}}})]})})]})},{Text:Rr}=Cr,Xd=e=>!e||e<=0?"":e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`,Wo=e=>{if(!e||e<=0)return"";const t=Math.floor(e/60),r=e%60;return`${t}:${r.toString().padStart(2,"0")}`},In=(e,t=120)=>!e||!e.includes("aliyuncs.com")?e:`${e.split("?")[0]}?x-oss-process=image/resize,w_${t},m_lfit/quality,q_80`,FE=new Set(["pdf","doc","docx","xls","xlsx","csv","ppt","pptx","txt","md","rtf"]),Jd=new Set(["jpg","jpeg","png","gif","bmp","webp","svg","ico","tiff","tif"]),Zd=new Set(["mp4","avi","mov","mkv","wmv","flv","webm","m4v"]),OE={pdf:{icon:pl,color:"#f5222d"},doc:{icon:_i,color:"#1890ff"},docx:{icon:_i,color:"#1890ff"},xls:{icon:vs,color:"#52c41a"},xlsx:{icon:vs,color:"#52c41a"},csv:{icon:vs,color:"#52c41a"},ppt:{icon:gi,color:"#fa8c16"},pptx:{icon:gi,color:"#fa8c16"},txt:{icon:Xs,color:"#8c8c8c"},log:{icon:Xs,color:"#8c8c8c"},rtf:{icon:Xs,color:"#8c8c8c"},md:{icon:Cu,color:"#595959"},zip:{icon:rn,color:"#faad14"},rar:{icon:rn,color:"#faad14"},"7z":{icon:rn,color:"#faad14"},tar:{icon:rn,color:"#faad14"},gz:{icon:rn,color:"#faad14"},js:{icon:rr,color:"#fadb14"},ts:{icon:rr,color:"#1890ff"},py:{icon:rr,color:"#389e0d"},go:{icon:rr,color:"#13c2c2"},java:{icon:rr,color:"#f5222d"}},RE=e=>{var i;const t=((i=e==null?void 0:e.split(".").pop())==null?void 0:i.toLowerCase())||"",r=OE[t],n=(r==null?void 0:r.icon)||Au,s=(r==null?void 0:r.color)||"#8c8c8c";return a.jsx(n,{style:{fontSize:20,color:s}})},ms=async(e,t)=>{try{const n=await(await fetch(e)).blob(),s=document.createElement("a");s.href=URL.createObjectURL(n),s.download=t,s.click(),URL.revokeObjectURL(s.href)}catch{window.open(e,"_blank")}},rc=({conversationType:e,messageTypes:t})=>{const{admin:r,permissions:n}=pt(),s=!!r&&!r.tenant_id&&!!(n!=null&&n.tenant_manage),{renderTenantId:i}=Yr(),o=e==="single",l=!!t&&t.length>0,[c]=b.useForm(),[d,u]=g.useState(!1),[h,m]=g.useState([]),[_,x]=g.useState(1),[f,p]=g.useState(20),[I,S]=g.useState(0),[w,v]=g.useState({}),[y,R]=g.useState("all"),[j,T]=g.useState("all"),[C,F]=g.useState(null),P={text:{label:"文本",color:"blue"},image:{label:"图片",color:"green"},video:{label:"视频",color:"purple"},audio:{label:"语音",color:"cyan"},file:{label:"文件",color:"orange"},contact_card:{label:"名片",color:"geekblue"},system:{label:"系统",color:"default"}},V=Q=>{if(!Q)return null;if(typeof Q=="object")return Q;try{return JSON.parse(Q)}catch{return null}},X=Q=>{const de=Q.is_revoked&&Q.original_content?Q.original_content:Q.content,k=V(de);if(!k)return typeof de=="string"?de:"--";switch(Q.type){case"text":return k.text||"--";case"image":return"[图片]";case"video":return k.file_name?`[视频] ${k.file_name}`:"[视频]";case"audio":return"[语音]";case"file":return k.file_name?`[文件] ${k.file_name}`:"[文件]";case"contact_card":return k.contact_name||k.contact_username||`ID:${k.contact_id||"?"}`;case"system":return k.system_msg?k.system_msg:(k.system_msg_data||k).type||"[系统消息]";default:return JSON.stringify(k)}},_e=Q=>{var Z;const de=Q.is_revoked&&Q.original_content?Q.original_content:Q.content,k=V(de);if(k){const me=k.url;if(l&&me){const ie=k.file_name||"",he=((Z=ie.split(".").pop())==null?void 0:Z.toLowerCase())||"";return Jd.has(he)||Q.type==="image"?a.jsx("div",{className:"admin-media-cell",children:a.jsxs(Ie,{size:8,align:"start",children:[a.jsx(Mr,{src:In(k.thumbnail||me),width:60,style:{maxHeight:60,objectFit:"cover",borderRadius:4},preview:{src:In(me,800)}}),a.jsx(Y,{type:"link",size:"small",icon:a.jsx($r,{}),onClick:()=>ms(me,ie||"图片"),children:"下载"})]})}):Zd.has(he)||Q.type==="video"?a.jsx("div",{className:"admin-media-cell",children:a.jsxs(Ie,{size:8,align:"start",children:[a.jsxs("div",{className:"admin-video-thumb",onClick:()=>F({type:"video",url:me,title:ie||"视频播放",poster:k.thumbnail?In(k.thumbnail,300):void 0}),children:[k.thumbnail?a.jsx("img",{src:In(k.thumbnail)}):a.jsx("div",{className:"admin-video-thumb-empty"}),a.jsx(dc,{className:"admin-video-play"})]}),a.jsx(Y,{type:"link",size:"small",icon:a.jsx($r,{}),onClick:()=>ms(me,ie||"视频"),children:"下载"})]})}):Q.type==="audio"?a.jsx("div",{className:"admin-media-cell",children:a.jsxs(Ie,{size:8,align:"center",children:[a.jsx(go,{style:{fontSize:20,color:"#1677ff",cursor:"pointer"},onClick:()=>F({type:"audio",url:me,title:"语音播放"})}),!!k.duration&&a.jsx(Rr,{type:"secondary",style:{fontSize:11},children:Wo(k.duration)})]})}):a.jsx("div",{className:"admin-media-cell",children:a.jsxs(Ie,{size:8,align:"center",children:[RE(ie),a.jsx(Y,{type:"link",size:"small",icon:a.jsx($r,{}),onClick:()=>ms(me,ie||"文件"),children:"下载"})]})})}switch(Q.type){case"image":if(me)return a.jsx("div",{className:"admin-media-cell",children:a.jsxs(Ie,{size:8,align:"start",children:[a.jsx(Mr,{src:In(k.thumbnail||me),width:60,style:{maxHeight:60,objectFit:"cover",borderRadius:4},preview:{src:In(me,800)}}),a.jsx(Y,{type:"link",size:"small",icon:a.jsx($r,{}),onClick:()=>ms(me,"图片"),children:"下载"})]})});break;case"video":if(me)return a.jsx("div",{className:"admin-media-cell",children:a.jsxs(Ie,{size:8,align:"start",children:[a.jsxs("div",{className:"admin-video-thumb",onClick:()=>F({type:"video",url:me,title:k.file_name||"视频播放",poster:k.thumbnail?In(k.thumbnail,300):void 0}),children:[k.thumbnail?a.jsx("img",{src:In(k.thumbnail)}):a.jsx("div",{className:"admin-video-thumb-empty"}),a.jsx(dc,{className:"admin-video-play"})]}),a.jsxs("div",{children:[a.jsx("div",{children:a.jsx(Rr,{ellipsis:{tooltip:!0},style:{maxWidth:160,fontSize:12},children:k.file_name||"视频"})}),!!k.duration&&a.jsx(Rr,{type:"secondary",style:{fontSize:11},children:Wo(k.duration)}),a.jsx("div",{children:a.jsx(Y,{type:"link",size:"small",icon:a.jsx($r,{}),onClick:()=>ms(me,k.file_name||"视频"),children:"下载"})})]})]})});break;case"audio":if(me)return a.jsx("div",{className:"admin-media-cell",children:a.jsxs(Ie,{size:8,align:"center",children:[a.jsx(go,{style:{fontSize:20,color:"#1677ff",cursor:"pointer"},onClick:()=>F({type:"audio",url:me,title:"语音播放"})}),!!k.duration&&a.jsx(Rr,{type:"secondary",style:{fontSize:11},children:Wo(k.duration)})]})});break;case"file":if(me)return a.jsx("div",{className:"admin-media-cell",children:a.jsxs("div",{children:[a.jsx("div",{children:a.jsx(Rr,{ellipsis:{tooltip:!0},style:{maxWidth:200,fontSize:12},children:k.file_name||"文件"})}),a.jsxs(Ie,{size:8,children:[!!k.file_size&&a.jsx(Rr,{type:"secondary",style:{fontSize:11},children:Xd(k.file_size)}),a.jsx(Y,{type:"link",size:"small",icon:a.jsx($r,{}),onClick:()=>ms(me,k.file_name||"文件"),children:"下载"})]})]})});break;case"contact_card":{if(s)return`ID:${k.contact_id||"?"}`;const ie=k.contact_name,he=k.contact_username;return ie||he?a.jsxs("div",{children:[a.jsx("div",{children:ie||he}),he&&ie&&a.jsx(Rr,{type:"secondary",style:{fontSize:12},children:he})]}):`ID:${k.contact_id||"?"}`}}}const W=X(Q),z=W.replace(/[\r\n]+/g," "),D=z.length>20,q=D?z.slice(0,20)+"...":z,K=()=>{Me.info({title:"消息内容",width:520,content:a.jsx("div",{style:{maxHeight:400,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"},children:W}),okText:"关闭"})};return a.jsxs("div",{style:{whiteSpace:"nowrap"},children:[a.jsx("span",{style:{color:"rgba(0,0,0,0.65)"},children:q}),D&&a.jsx(Y,{type:"link",size:"small",style:{padding:"0 0 0 4px",height:"auto",fontSize:12},onClick:K,children:"详情"})]})},B=async(Q=_,de=f,k=w,W)=>{u(!0);const z=W??(l?y:j);let D=t,q;l&&z!=="all"?z==="document"||z==="other"?(D=["file"],q=z):D=[z]:!l&&z!=="all"&&(D=[z]);try{const K=await ed.search({...k,conversation_type:e,message_types:D==null?void 0:D.join(","),file_category:q,page:Q,page_size:de});(K.code===0||K.code===200)&&K.data?(m(K.data.items),S(K.data.total),x(K.data.page),p(K.data.size)):E.error(K.message||"获取消息列表失败")}catch(K){E.error(K.message||"获取消息列表失败")}finally{u(!1)}};g.useEffect(()=>{e||B(1,f,w)},[e]);const te=Q=>{const de={tenant_id:Q.tenant_id,conversation_id:Q.conversation_id,sender_id:Q.sender_id,receiver_id:Q.receiver_id,sender_username:Q.sender_username,receiver_username:Q.receiver_username,keyword:Q.keyword,start_time:Q.startTime?Q.startTime.format("YYYY-MM-DD HH:mm:ss"):void 0,end_time:Q.endTime?Q.endTime.format("YYYY-MM-DD HH:mm:ss"):void 0};v(de),B(1,f,de)},J=Q=>{Me.confirm({title:"确认删除该消息?",content:l?"删除后该文件消息将从聊天中实时消失,后台仍保留记录。":o?"删除后该消息将从双方聊天列表中实时消失,后台仍保留记录。":"删除后该消息将从群聊中实时消失,后台仍保留记录。",okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const de=await ed.delete(Q.id);de.code===0||de.code===200?(E.success("消息已删除"),B()):E.error(de.message||"删除失败")}catch(de){E.error(de.message||"删除失败")}}})},G=(Q,de)=>{if(!Q)return de||"--";const k=Q.nickname||Q.username||"--",W=Q.username||(s?`ID:${Q.id}`:"");return a.jsxs("div",{children:[a.jsx("div",{children:k}),W&&a.jsx(Rr,{type:"secondary",style:{fontSize:12},children:W})]})},se=[...s?[{title:"ID",dataIndex:"id",width:80},{title:"会话 ID",dataIndex:"conversation_id",width:100},{title:"租户",dataIndex:"tenant_id",width:160,render:i},{title:"SEQ",dataIndex:"seq",width:80}]:[],{title:"发送人",dataIndex:"sender",width:160,render:(Q,de)=>G(de.sender,de.sender_id)},...l?[{title:"接收方",key:"target",width:160,render:(Q,de)=>de.conversation_type==="group"?de.group_name?a.jsxs("div",{children:[a.jsx("div",{children:de.group_name}),!!de.group_code&&a.jsx(Rr,{type:"secondary",style:{fontSize:12},children:de.group_code})]}):"--":G(de.receiver)}]:[...o?[{title:"接收人",dataIndex:"receiver",width:160,render:(Q,de)=>G(de.receiver)}]:[],...e==="group"?[{title:"群名称",dataIndex:"group_name",width:160,render:(Q,de)=>de.group_name?a.jsxs("div",{children:[a.jsx("div",{children:de.group_name}),!!de.group_code&&a.jsx(Rr,{type:"secondary",style:{fontSize:12},children:de.group_code})]}):"--"}]:[]],{title:"类型",dataIndex:"type",width:80,render:(Q,de)=>{var W;if(l){const z=V(de.content),D=z==null?void 0:z.file_name,q=((W=D==null?void 0:D.split(".").pop())==null?void 0:W.toLowerCase())||"";return Jd.has(q)||Q==="image"?a.jsx(Te,{color:"green",children:"图片"}):Zd.has(q)||Q==="video"?a.jsx(Te,{color:"blue",children:"视频"}):Q==="audio"?a.jsx(Te,{color:"purple",children:"音频"}):FE.has(q)?a.jsx(Te,{color:"orange",children:"文档"}):a.jsx(Te,{color:"default",children:"其他"})}const k=P[Q];return k?a.jsx(Te,{color:k.color,children:k.label}):a.jsx(Te,{children:Q})}},{title:"内容",dataIndex:"content",width:l?140:300,render:(Q,de)=>_e(de)},...l?[{title:"文件名",key:"file_name",width:260,render:(Q,de)=>{const k=V(de.content),W=k==null?void 0:k.file_name;return W?a.jsx(Rr,{ellipsis:{tooltip:!0},style:{maxWidth:240},children:W}):"--"}},{title:"文件大小",key:"file_size",width:100,render:(Q,de)=>{const k=V(de.content),W=k==null?void 0:k.file_size;return W?Xd(W):"--"}},{title:"URL",key:"file_url",width:200,render:(Q,de)=>{const k=V(de.content),W=k==null?void 0:k.url;return W?a.jsx(Xt,{title:W,children:a.jsx(Rr,{copyable:!0,ellipsis:!0,style:{maxWidth:180,fontSize:12},children:W})}):"--"}}]:[],{title:"状态",dataIndex:"is_revoked",width:80,render:(Q,de)=>de.is_admin_deleted?a.jsx(Te,{color:"default",children:"已删除"}):de.is_revoked?a.jsx(Te,{color:"orange",children:"已撤回"}):a.jsx(Te,{color:"green",children:"正常"})},{title:"发送时间",dataIndex:"created_at",width:170,render:Q=>Ke(Q)},{title:"操作",key:"actions",width:80,render:(Q,de)=>de.is_admin_deleted?a.jsx(Te,{color:"default",children:"已删除"}):a.jsx(Y,{size:"small",danger:!0,onClick:()=>J(de),children:"删除"})}];return a.jsxs("div",{className:"admin-messages",children:[a.jsxs(be,{className:"admin-messages-filter",children:[l?a.jsx(qo,{options:[{label:"全部",value:"all"},{label:"图片",value:"image"},{label:"视频",value:"video"},{label:"音频",value:"audio"},{label:"文档",value:"document"},{label:"其他",value:"other"}],value:y,onChange:Q=>{R(Q),B(1,f,w,Q)},style:{marginBottom:12}}):a.jsx(qo,{options:[{label:"全部",value:"all"},{label:"文本",value:"text"},{label:"图片",value:"image"},{label:"视频",value:"video"},{label:"语音",value:"audio"},{label:"文件",value:"file"},{label:"名片",value:"contact_card"},{label:"系统",value:"system"}],value:j,onChange:Q=>{T(Q),B(1,f,w,Q)},style:{marginBottom:12}}),a.jsxs(b,{form:c,layout:"inline",onFinish:te,children:[s&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),s?a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"会话 ID",name:"conversation_id",children:a.jsx(at,{placeholder:"可选",min:1})}),a.jsx(b.Item,{label:"发送人 ID",name:"sender_id",children:a.jsx(at,{placeholder:"可选",min:1})}),o&&a.jsx(b.Item,{label:"接收人 ID",name:"receiver_id",children:a.jsx(at,{placeholder:"可选",min:1})})]}):a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"发送人用户名",name:"sender_username",children:a.jsx(ve,{placeholder:"可选",allowClear:!0})}),o&&!l&&a.jsx(b.Item,{label:"接收人用户名",name:"receiver_username",children:a.jsx(ve,{placeholder:"可选",allowClear:!0})})]}),a.jsx(b.Item,{label:l?"文件名":"关键词",name:"keyword",children:a.jsx(ve,{placeholder:l?"搜索文件名":"关键字",allowClear:!0})}),a.jsx(b.Item,{label:"开始时间",name:"startTime",children:a.jsx(jr,{showTime:!0,placeholder:"开始时间",style:{width:200}})}),a.jsx(b.Item,{label:"结束时间",name:"endTime",children:a.jsx(jr,{showTime:!0,placeholder:"结束时间",style:{width:200}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>{c.resetFields(),T("all"),R("all")},children:"重置"}),a.jsx(Y,{onClick:()=>B(),children:"刷新"})]})})]})]}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:se,dataSource:h,loading:d,scroll:{x:l?1800:1400},pagination:{current:_,pageSize:f,total:I,showSizeChanger:!0,onChange:(Q,de)=>{x(Q),p(de),B(Q,de)}}})}),a.jsxs(Me,{open:!!C,onCancel:()=>F(null),footer:null,centered:!0,destroyOnClose:!0,width:(C==null?void 0:C.type)==="video"?680:420,title:C==null?void 0:C.title,children:[(C==null?void 0:C.type)==="video"&&a.jsx("video",{controls:!0,autoPlay:!0,src:C.url,poster:C.poster,style:{width:"100%",borderRadius:8,background:"#000"}}),(C==null?void 0:C.type)==="audio"&&a.jsxs("div",{style:{padding:"24px 0",textAlign:"center"},children:[a.jsx(go,{style:{fontSize:48,color:"#1677ff",marginBottom:16}}),a.jsx("audio",{controls:!0,autoPlay:!0,src:C.url,style:{width:"100%"}})]})]})]})},DE=()=>a.jsx(rc,{conversationType:"single"}),PE=()=>a.jsx(rc,{conversationType:"group"}),{Text:Zr}=Cr,kh=[{label:"搜索",value:"search"},{label:"导出",value:"export"},{label:"创建用户",value:"create_user"},{label:"批量创建用户",value:"batch_create_users"},{label:"处理举报",value:"handle_report"},{label:"更新状态",value:"update_status"},{label:"重置密码",value:"reset_password"},{label:"修改用户名",value:"update_username"},{label:"更新安全权限",value:"update_security_perm"},{label:"强制下线",value:"kick_user"},{label:"解散群组",value:"dismiss_group"},{label:"封禁群组",value:"ban_group"},{label:"批量封禁用户",value:"batch_ban_users"},{label:"按注册IP封禁用户(历史)",value:"ban_users_by_register_ip"},{label:"按IP封禁用户",value:"ban_users_by_ip"},{label:"解封群组",value:"unban_group"},{label:"删除消息",value:"delete_message"},{label:"踢出群成员",value:"kick_group_member"},{label:"禁言群成员",value:"mute_group_member"},{label:"解除禁言",value:"unmute_group_member"},{label:"强制注销",value:"force_deactivate"},{label:"更新安全策略",value:"update_security_settings"},{label:"创建用户分组",value:"create_policy_group"},{label:"更新用户分组",value:"update_policy_group"},{label:"删除用户分组",value:"delete_policy_group"},{label:"调整用户分组",value:"assign_policy_group"},{label:"更新分组IP白名单",value:"update_policy_ip_whitelist"},{label:"删除群文件",value:"delete_group_file"}],Fh=[{label:"用户",value:"user"},{label:"群组",value:"group"},{label:"消息",value:"message"},{label:"举报",value:"report"},{label:"租户",value:"tenant"},{label:"会话",value:"conversation"},{label:"用户分组",value:"user_policy_group"}],NE=Object.fromEntries(kh.map(e=>[e.value,e.label])),LE=Object.fromEntries(Fh.map(e=>[e.value,e.label])),ME={tenant_id:"租户",user_id:"用户",group_id:"群组",keyword:"关键词",conversation_type:"会话类型",start_time:"开始时间",end_time:"结束时间",sender_username:"发送人",receiver_username:"接收人",reason:"原因",target_member:"操作成员"},BE={conversation_type:{single:"单聊",group:"群聊"}},Qd=(e,t=120)=>!e||!e.includes("aliyuncs.com")?e:`${e.split("?")[0]}?x-oss-process=image/resize,w_${t},m_lfit/quality,q_80`,UE=e=>{const t=e.match(/^\[(图片|语音|视频)]\s+(https?:\/\/\S+)/);return t?{type:{图片:"image",语音:"audio",视频:"video"}[t[1]]||null,url:t[2],label:t[1]}:{type:null,url:"",label:e}},zE=e=>!!(e==null||e===""||e===0||typeof e=="string"&&e.startsWith("0001-01-01")),mi=e=>{const t=e.lastIndexOf("(");return t>0&&e.endsWith(")")?e.slice(0,t):e},$E=(e,t)=>{const r=String(t),n=BE[e];return n&&n[r]?n[r]:e==="target_member"||e==="sender_username"||e==="receiver_username"?mi(r):(e==="start_time"||e==="end_time")&&typeof t=="string"&&t.includes("T")?r.replace("T"," ").replace("Z","").replace(/:\d{2}$/,""):r},WE=(e,t)=>{if(!e)return"--";const r=[];for(const[n,s]of Object.entries(e)){if(zE(s)||t!=null&&t.has(n))continue;const i=ME[n]||n;r.push(`${i}: ${$E(n,s)}`)}return r.length>0?r.join(" "):"--"},HE=()=>{const{admin:e,permissions:t}=pt(),r=!(e!=null&&e.tenant_id),{renderTenantId:n}=Yr(),[s,i]=g.useState(!1),[o,l]=g.useState(!1),[c,d]=g.useState([]),[u,h]=g.useState(1),[m,_]=g.useState(20),[x,f]=g.useState(0),[p,I]=g.useState({}),[S]=b.useForm(),w=async(T=u,C=m,F=p)=>{i(!0);try{const P=await td.list({...F,page:T,page_size:C});(P.code===0||P.code===200)&&P.data?(d(P.data.items),f(P.data.total),h(P.data.page),_(P.data.size)):E.error(P.message||"获取审计日志失败")}catch(P){E.error(P.message||"获取审计日志失败")}finally{i(!1)}};g.useEffect(()=>{w(1,m,p)},[]);const v=T=>{const C={};return T.admin_id&&(C.admin_id=T.admin_id),T.tenant_id&&(C.tenant_id=T.tenant_id),T.action&&(C.action=T.action),T.target_type&&(C.target_type=T.target_type),r?(T.target_user_id&&(C.target_user_id=T.target_user_id),T.target_group_id&&(C.target_group_id=T.target_group_id)):(T.target_username&&(C.target_username=T.target_username),T.target_group_code&&(C.target_group_code=T.target_group_code)),T.startTime&&(C.start_time=T.startTime.format("YYYY-MM-DD HH:mm:ss")),T.endTime&&(C.end_time=T.endTime.format("YYYY-MM-DD HH:mm:ss")),C},y=T=>{const C=v(T);I(C),w(1,m,C)},R=async()=>{const T=S.getFieldsValue(),C=v(T);l(!0);try{await td.exportCsv(C),E.success("导出成功")}catch(F){E.error(F.message||"导出失败")}finally{l(!1)}},j=[r?{title:"ID",dataIndex:"id",width:80}:null,{title:"管理员",dataIndex:"admin_name",width:160},r?{title:"租户",dataIndex:"tenant_id",width:160,render:n}:null,{title:"动作",dataIndex:"action",width:120,render:T=>a.jsx(Te,{color:"blue",children:NE[T]||T})},{title:"目标类型",dataIndex:"target_type",width:90,render:T=>a.jsx(Te,{color:"geekblue",children:LE[T]||T})},r?{title:"目标 ID",dataIndex:"target_id",width:100,render:T=>T??"--"}:{title:"目标",dataIndex:"target_display",width:160,render:(T,C)=>{const F=C.target_display;return F?a.jsxs("div",{children:[a.jsx("div",{children:F.name}),F.sub&&a.jsx(Zr,{type:"secondary",style:{fontSize:12},children:F.sub})]}):C.target_id!=null?String(C.target_id):"--"}},{title:"查询条件",dataIndex:"query",width:260,render:T=>{const C=T==null?void 0:T.details,F=T==null?void 0:T.reason,P=T==null?void 0:T.sender_username,V=T==null?void 0:T.receiver_username,X=T==null?void 0:T.target_member,_e=new Set(["details","group_id","reason","target_member"]);C&&(_e.add("sender_username"),_e.add("receiver_username")),r||_e.add("tenant_id");const B=WE(T,_e),te=!!(C||F||X);return a.jsxs(Ie,{direction:"vertical",size:0,children:[B!=="--"?a.jsx(Xt,{title:B,children:a.jsx(Zr,{type:"secondary",ellipsis:!0,className:"admin-audit-query",children:B})}):te?null:"--",te&&a.jsx(Y,{type:"link",size:"small",style:{padding:0,height:"auto",fontSize:12},onClick:()=>{if(!P&&!V&&!X&&(C!=null&&C.includes(" "))){const G=C.split(` +`).filter(Boolean).map((se,Q)=>{const de=se.split(" ");return{key:Q,nickname:de[0]||se,username:de[1]||""}});Me.info({title:"操作详情",width:480,content:a.jsxs("div",{children:[a.jsx("div",{style:{maxHeight:360,overflow:"auto"},children:a.jsx(Ye,{size:"small",pagination:!1,rowKey:"key",dataSource:G,columns:[{title:"#",dataIndex:"key",width:40,render:(se,Q,de)=>a.jsx(Zr,{type:"secondary",children:de+1})},{title:"昵称",dataIndex:"nickname"},{title:"用户名",dataIndex:"username"}]})}),a.jsx("div",{style:{textAlign:"right",marginTop:8},children:a.jsx(Y,{size:"small",onClick:()=>{const se=G.map(Q=>Q.username?`${Q.nickname} ${Q.username}`:Q.nickname).join(` +`);navigator.clipboard.writeText(se).then(()=>E.success("已复制"))},children:"复制全部"})})]}),okText:"关闭"})}else{const J=X?mi(X):void 0,G=X&&X.lastIndexOf("(")>0&&X.endsWith(")")?X.slice(X.lastIndexOf("(")+1,-1):void 0;Me.info({title:"操作详情",width:480,content:a.jsxs("div",{children:[J&&a.jsxs("div",{style:{marginBottom:4},children:[a.jsx(Zr,{type:"secondary",children:"操作成员:"}),J,G&&a.jsxs(Zr,{type:"secondary",style:{marginLeft:8},children:["(",G,")"]})]}),P&&a.jsxs("div",{style:{marginBottom:4},children:[a.jsx(Zr,{type:"secondary",children:"发送人:"}),mi(P)]}),V&&a.jsxs("div",{style:{marginBottom:4},children:[a.jsx(Zr,{type:"secondary",children:"接收人:"}),mi(V)]}),F&&a.jsxs("div",{style:{marginBottom:4},children:[a.jsx(Zr,{type:"secondary",children:"原因:"}),F]}),C&&(()=>{const se=UE(C);return se.type==="image"&&se.url?a.jsxs("div",{style:{marginTop:8},children:[a.jsxs(Zr,{type:"secondary",children:["消息内容:[",se.label,"]"]}),a.jsx("div",{style:{marginTop:4},children:a.jsx(Mr,{src:Qd(se.url,400),style:{maxWidth:360,maxHeight:300,borderRadius:4},preview:{src:Qd(se.url,800)}})})]}):(se.type==="video"||se.type==="audio")&&se.url?a.jsxs("div",{style:{marginTop:8},children:[a.jsxs(Zr,{type:"secondary",children:["消息内容:[",se.label,"]"]}),a.jsx("div",{style:{marginTop:4},children:a.jsx("a",{href:se.url,target:"_blank",rel:"noopener noreferrer",children:se.url})})]}):a.jsx("div",{style:{marginTop:8,padding:8,background:"#fafafa",borderRadius:4,maxHeight:300,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"},children:C})})()]}),okText:"关闭"})}},children:"详情"})]})}},{title:"IP",dataIndex:"ip",width:140},{title:"时间",dataIndex:"created_at",width:180,render:T=>Ke(T)}].filter(T=>T!=null);return a.jsxs("div",{className:"admin-audit",children:[a.jsx(be,{className:"admin-audit-filter",children:a.jsxs(b,{form:S,layout:"inline",onFinish:y,children:[r&&a.jsx(b.Item,{label:"管理员 ID",name:"admin_id",children:a.jsx(at,{placeholder:"可选",min:1})}),r&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"动作",name:"action",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:kh,style:{width:120}})}),a.jsx(b.Item,{label:"目标类型",name:"target_type",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:Fh,style:{width:120}})}),r?a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"用户 ID",name:"target_user_id",children:a.jsx(at,{placeholder:"可选",min:1})}),a.jsx(b.Item,{label:"群组 ID",name:"target_group_id",children:a.jsx(at,{placeholder:"可选",min:1})})]}):a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"目标用户",name:"target_username",children:a.jsx(ve,{placeholder:"用户名",allowClear:!0,style:{width:130}})}),a.jsx(b.Item,{label:"目标群",name:"target_group_code",children:a.jsx(ve,{placeholder:"群号",allowClear:!0,style:{width:130}})})]}),a.jsx(b.Item,{label:"开始时间",name:"startTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{label:"结束时间",name:"endTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:()=>S.resetFields(),children:"重置"}),a.jsx(Y,{icon:a.jsx($r,{}),onClick:R,loading:o,disabled:!(t!=null&&t.export_chat),children:"导出 CSV"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:j,dataSource:c,loading:s,scroll:{x:1200},pagination:{current:u,pageSize:m,total:x,showSizeChanger:!0,onChange:(T,C)=>{h(T),_(C),w(T,C)}}})})]})},VE=[{label:"启用",value:"active"},{label:"封禁",value:"banned"}],KE=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(1),[o,l]=g.useState(20),[c,d]=g.useState(0),[u,h]=g.useState({}),[m]=b.useForm(),[_,x]=g.useState(!1),[f,p]=g.useState(null),[I]=b.useForm(),[S,w]=g.useState({}),[v,y]=g.useState([]),R=async(K=s,Z=o,me=u)=>{t(!0);try{const ie=await hn.list({...me,page:K,page_size:Z});(ie.code===0||ie.code===200)&&ie.data?(n(ie.data.items),d(ie.data.total),i(ie.data.page),l(ie.data.size),j(ie.data.items)):E.error(ie.message||"获取私域列表失败")}catch(ie){E.error(ie.message||"获取私域列表失败")}finally{t(!1)}},j=g.useCallback(async K=>{const Z={};(await Promise.allSettled(K.map(ie=>On.getTenantBinding(ie.id)))).forEach((ie,he)=>{if(ie.status==="fulfilled"){const we=ie.value;(we.code===0||we.code===200)&&we.data&&(Z[K[he].id]=we.data)}}),w(Z)},[]),T=async()=>{try{const K=await On.list({page:1,page_size:100});(K.code===0||K.code===200)&&K.data&&y(K.data.items||[])}catch{}};g.useEffect(()=>{R(1,o,u),T()},[]);const C=K=>{const Z={tenant_id:K.tenant_id,status:K.status,keyword:K.keyword};h(Z),R(1,o,Z)},F=g.useMemo(()=>v.find(K=>K.is_default),[v]),P=g.useMemo(()=>{const K=[];return F&&F.status==="active"&&K.push({label:`${F.name}(中心服务器)`,value:F.node_id}),v.filter(Z=>!Z.is_default&&Z.status==="active"&&Z.tenant_count===0).forEach(Z=>K.push({label:`${Z.name}(${Z.node_id})`,value:Z.node_id})),K},[v,F]),V=()=>{p(null),I.resetFields(),x(!0)},X=K=>{p(K),I.setFieldsValue({name:K.name,username_prefix:K.username_prefix,default_password:"",max_users:K.max_users}),x(!0)},_e=async()=>{try{const K=await I.validateFields();let Z;f?Z=await hn.update(f.id,{name:K.name,username_prefix:K.username_prefix,default_password:K.default_password||void 0,max_users:K.max_users}):Z=await hn.create({code:K.code,name:K.name,username_prefix:K.username_prefix,default_password:K.default_password,max_users:K.max_users,node_id:K.node_id,admin_username:K.admin_username,admin_password:K.admin_password}),Z.code===0||Z.code===200?(E.success(f?"私域已更新":"私域已创建"),x(!1),R()):E.error(Z.message||"操作失败")}catch(K){if(K!=null&&K.errorFields)return;E.error(K.message||"操作失败")}},[B,te]=g.useState(!1),[J,G]=g.useState(null),[se,Q]=g.useState(""),de=K=>{G(K),Q(""),te(!0)},k=async()=>{if(J)try{const K=await hn.updateStatus(J.id,"banned",se);K.code===0||K.code===200?(E.success("私域已封禁,所有在线用户已被踢下线"),te(!1),R()):E.error(K.message||"封禁失败")}catch(K){E.error(K.message||"封禁失败")}},W=async K=>{try{const Z=await hn.updateStatus(K.id,"active");Z.code===0||Z.code===200?(E.success("私域已启用"),R()):E.error(Z.message||"启用失败")}catch(Z){E.error(Z.message||"启用失败")}},z=()=>{Me.confirm({title:"自动绑定",content:"将所有未绑定节点的私域自动绑定到默认节点,确认操作?",okText:"确认",cancelText:"取消",onOk:async()=>{var K;try{const Z=await hn.autoBind();Z.code===0||Z.code===200?(E.success(`已自动绑定 ${((K=Z.data)==null?void 0:K.bound_count)??0} 个私域`),R(),T()):E.error(Z.message||"自动绑定失败")}catch(Z){E.error(Z.message||"自动绑定失败")}}})},D=K=>{Me.confirm({title:"确认踢出节点?",content:`将解除私域「${K.name}」的节点绑定关系,解绑后该私域将不可用。`,okText:"踢出",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const Z=await On.unbindTenant(K.id);Z.code===0||Z.code===200?(E.success("已踢出节点"),j(r),T()):E.error(Z.message||"踢出失败")}catch(Z){E.error(Z.message||"踢出失败")}}})},q=[{title:"ID",dataIndex:"id",width:80},{title:"编号",dataIndex:"tenant_no",width:80},{title:"私域编码",dataIndex:"code",width:140},{title:"私域名称",dataIndex:"name",width:160},{title:"管理员",dataIndex:"admin_username",width:120,render:K=>K||"-"},{title:"前缀",dataIndex:"username_prefix",width:100},{title:"人数",key:"user_count",width:120,render:(K,Z)=>{const me=Z.max_users>0?Z.max_users:"∞";return`${Z.user_count} / ${me}`}},{title:"状态",dataIndex:"status",width:100,render:K=>a.jsx(Te,{color:K==="active"?"green":"red",children:K==="active"?"启用":"封禁"})},{title:"绑定节点",key:"binding",width:160,render:(K,Z)=>{const me=S[Z.id];return me?a.jsx(Te,{color:"blue",children:me.node_name||me.node_id}):a.jsx(Te,{children:"未绑定"})}},{title:"创建时间",dataIndex:"created_at",width:170,render:K=>Ke(K)},{title:"操作",key:"actions",width:240,render:(K,Z)=>{const me=S[Z.id];return a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>X(Z),children:"编辑"}),Z.status==="active"?a.jsx(Y,{size:"small",danger:!0,onClick:()=>de(Z),children:"封禁"}):a.jsx(Y,{size:"small",onClick:()=>W(Z),children:"启用"}),me&&a.jsx(Y,{size:"small",danger:!0,onClick:()=>D(Z),children:"踢出节点"})]})}}];return a.jsxs("div",{className:"admin-tenants",children:[a.jsx(be,{className:"admin-tenants-filter",children:a.jsxs(b,{form:m,layout:"inline",onFinish:C,children:[a.jsx(b.Item,{label:"私域 ID",name:"tenant_id",children:a.jsx(at,{placeholder:"可选",min:1})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:VE,style:{width:120}})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"私域名称 / 编码",allowClear:!0})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>m.resetFields(),children:"重置"}),a.jsx(Y,{onClick:V,children:"创建私域"}),a.jsx(Y,{onClick:z,children:"自动绑定"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:q,dataSource:r,loading:e,scroll:{x:1600},pagination:{current:s,pageSize:o,total:c,showSizeChanger:!0,onChange:(K,Z)=>{i(K),l(Z),R(K,Z)}}})}),a.jsx(Me,{title:f?"编辑私域":"创建私域",open:_,onCancel:()=>x(!1),onOk:_e,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:I,layout:"vertical",children:[!f&&a.jsx(b.Item,{label:"私域编码",name:"code",rules:[{required:!0,message:"请输入私域编码"}],children:a.jsx(ve,{placeholder:"例如 TEST01"})}),a.jsx(b.Item,{label:"私域名称",name:"name",rules:[{required:!0,message:"请输入私域名称"}],children:a.jsx(ve,{placeholder:"私域名称"})}),a.jsx(b.Item,{label:"用户名前缀",name:"username_prefix",rules:[{required:!0,message:"请输入用户名前缀"}],children:a.jsx(ve,{placeholder:"2-4 位前缀",maxLength:4})}),a.jsx(b.Item,{label:"默认密码",name:"default_password",children:a.jsx(ve.Password,{placeholder:f?"留空表示不修改":"可选"})}),f&&a.jsx(b.Item,{label:`最大用户数(当前 ${f.user_count} 人)`,name:"max_users",rules:[{required:!0,message:"请输入最大用户数"},{type:"number",min:1,message:"最大用户数必须大于 0"}],children:a.jsx(at,{min:1,placeholder:"请输入大于 0 的数值",style:{width:"100%"}})}),!f&&a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"最大用户数",name:"max_users",rules:[{required:!0,message:"请输入最大用户数"},{type:"number",min:1,message:"最大用户数必须大于 0"}],children:a.jsx(at,{min:1,placeholder:"请输入大于 0 的数值",style:{width:"100%"}})}),a.jsx(b.Item,{label:"节点",name:"node_id",initialValue:F==null?void 0:F.node_id,extra:"默认为中心服务器,子节点 1:1 绑定",children:a.jsx($e,{placeholder:"选择节点",options:P})}),a.jsx(b.Item,{label:"管理员用户名",name:"admin_username",rules:[{required:!0,message:"请输入管理员用户名"},{max:20,message:"用户名不超过 20 位"},{pattern:/^[a-z0-9]+$/,message:"仅限小写字母和数字"}],extra:"私域最高管理员账号,小写字母+数字,20 位以内",children:a.jsx(ve,{placeholder:"小写字母+数字",maxLength:20})}),a.jsx(b.Item,{label:"管理员密码",name:"admin_password",rules:[{required:!0,message:"请输入管理员密码"},{min:8,max:20,message:"密码需为 8-20 位"},{pattern:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/,message:"需包含数字、大写和小写字母"}],extra:"数字 + 大小写字母,8-20 位",children:a.jsx(ve.Password,{placeholder:"8-20 位"})})]})]})}),a.jsxs(Me,{title:`封禁私域「${(J==null?void 0:J.name)||""}」`,open:B,onCancel:()=>te(!1),onOk:k,okText:"确认封禁",okButtonProps:{danger:!0},cancelText:"取消",destroyOnClose:!0,children:[a.jsx("p",{style:{marginBottom:8,color:"#666"},children:"封禁后,该私域所有在线用户将被踢下线,且无法再登录。"}),a.jsx(ve.TextArea,{rows:3,placeholder:"请输入封禁原因",value:se,onChange:K=>Q(K.target.value),maxLength:200,showCount:!0})]})]})},ks=[{label:"私域管理",value:"tenant_manage"},{label:"用户管理",value:"user_manage"},{label:"群组管理",value:"group_manage"},{label:"私聊审计",value:"view_private_chat"},{label:"群聊审计",value:"view_group_chat"},{label:"导出记录",value:"export_chat"},{label:"操作日志",value:"view_audit_log"}],Fs=e=>e?ks.filter(t=>e[t.value]).map(t=>t.value):[],nc=(e=[])=>({tenant_manage:e.includes("tenant_manage"),user_manage:e.includes("user_manage"),group_manage:e.includes("group_manage"),view_private_chat:e.includes("view_private_chat"),view_group_chat:e.includes("view_group_chat"),export_chat:e.includes("export_chat"),view_audit_log:e.includes("view_audit_log")}),GE=[{label:"启用",value:"active"},{label:"禁用",value:"disabled"}],YE=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(1),[o,l]=g.useState(20),[c,d]=g.useState(0),[u,h]=g.useState({}),[m]=b.useForm(),[_,x]=g.useState(!1),[f,p]=g.useState(null),[I]=b.useForm(),[S,w]=g.useState(!1),[v,y]=g.useState(null),[R]=b.useForm(),j=async(G=s,se=o,Q=u)=>{t(!0);try{const de=await os.list({...Q,page:G,page_size:se});(de.code===0||de.code===200)&&de.data?(n(de.data.items),d(de.data.total),i(de.data.page),l(de.data.size)):E.error(de.message||"获取管理员列表失败")}catch(de){E.error(de.message||"获取管理员列表失败")}finally{t(!1)}};g.useEffect(()=>{j(1,o,u)},[]);const T=G=>{const se={status:G.status,keyword:G.keyword};h(se),j(1,o,se)},C=()=>{p(null),I.resetFields(),I.setFieldsValue({permissions:[]}),x(!0)},F=G=>{p(G),I.setFieldsValue({nickname:G.nickname,permissions:Fs(G.permissions)}),x(!0)},P=async()=>{try{const G=await I.validateFields(),se=nc(G.permissions||[]),Q=f?await os.update(f.id,{nickname:G.nickname,permissions:se}):await os.create({username:G.username,password:G.password,nickname:G.nickname,permissions:se});Q.code===0||Q.code===200?(E.success(f?"管理员已更新":"管理员已创建"),x(!1),j()):E.error(Q.message||"操作失败")}catch(G){if(G!=null&&G.errorFields)return;E.error(G.message||"操作失败")}},V=async G=>{const se=G.status==="active"?"disabled":"active";try{const Q=await os.updateStatus(G.id,se);Q.code===0||Q.code===200?(E.success("状态已更新"),j()):E.error(Q.message||"更新状态失败")}catch(Q){E.error(Q.message||"更新状态失败")}},X=G=>{y(G),R.resetFields(),w(!0)},_e=async()=>{try{const G=await R.validateFields(),se=await os.resetPassword(v.id,G.password);se.code===0||se.code===200?(E.success("密码已重置"),w(!1)):E.error(se.message||"重置密码失败")}catch(G){if(G!=null&&G.errorFields)return;E.error(G.message||"重置密码失败")}},B=G=>{Me.confirm({title:"确认删除该管理员?",content:"删除后将无法登录后台,请谨慎操作。",okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const se=await os.delete(G.id);se.code===0||se.code===200?(E.success("管理员已删除"),j()):E.error(se.message||"删除失败")}catch(se){E.error(se.message||"删除失败")}}})},te=G=>{const se=Fs(G);return se.length?a.jsx(Ie,{wrap:!0,children:se.map(Q=>{var k;const de=((k=ks.find(W=>W.value===Q))==null?void 0:k.label)||Q;return a.jsx(Te,{color:"blue",children:de},Q)})}):"--"},J=[{title:"ID",dataIndex:"id",width:80},{title:"用户名",dataIndex:"username",width:160},{title:"昵称",dataIndex:"nickname",width:160},{title:"权限",dataIndex:"permissions",width:320,render:(G,se)=>te(se.permissions)},{title:"状态",dataIndex:"status",width:100,render:G=>a.jsx(Te,{color:G==="active"?"green":"red",children:G==="active"?"启用":"禁用"})},{title:"最近登录",dataIndex:"last_login_at",width:180,render:G=>Ke(G)},{title:"创建时间",dataIndex:"created_at",width:180,render:G=>Ke(G)},{title:"操作",key:"actions",width:300,render:(G,se)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>F(se),children:"编辑"}),a.jsx(Y,{size:"small",onClick:()=>X(se),children:"重置密码"}),a.jsx(Y,{size:"small",onClick:()=>V(se),children:se.status==="active"?"禁用":"启用"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>B(se),children:"删除"})]})}];return a.jsxs("div",{className:"admin-admins",children:[a.jsx(be,{className:"admin-admins-filter",children:a.jsxs(b,{form:m,layout:"inline",onFinish:T,children:[a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:GE,style:{width:120}})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"用户名/昵称",allowClear:!0})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>m.resetFields(),children:"重置"}),a.jsx(Y,{onClick:C,children:"创建管理员"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:J,dataSource:r,loading:e,scroll:{x:1200},pagination:{current:s,pageSize:o,total:c,showSizeChanger:!0,onChange:(G,se)=>{i(G),l(se),j(G,se)}}})}),a.jsx(Me,{title:f?"编辑管理员":"创建管理员",open:_,onCancel:()=>x(!1),onOk:P,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:I,layout:"vertical",children:[!f&&a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"用户名",name:"username",rules:[{required:!0,message:"请输入用户名"}],children:a.jsx(ve,{placeholder:"请输入用户名"})}),a.jsx(b.Item,{label:"密码",name:"password",tooltip:"至少 8 位,需包含大小写字母和数字",rules:[{required:!0,message:"请输入密码"},{min:8,message:"密码至少 8 位"},{pattern:/^[\x21-\x7e]+$/,message:"只能包含英文字母、数字和英文特殊字符"},{pattern:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/,message:"必须包含大小写字母和数字"}],children:a.jsx(ve.Password,{placeholder:"请输入密码"})})]}),a.jsx(b.Item,{label:"昵称",name:"nickname",rules:[{required:!0,message:"请输入昵称"}],children:a.jsx(ve,{placeholder:"管理员昵称"})}),a.jsx(b.Item,{label:"权限",name:"permissions",rules:[{required:!0,message:"请选择权限"}],children:a.jsx(gl.Group,{options:ks})})]})}),a.jsx(Me,{title:`重置密码 - ${(v==null?void 0:v.username)||""}`,open:S,onCancel:()=>w(!1),onOk:_e,okText:"确认重置",cancelText:"取消",destroyOnClose:!0,children:a.jsx(b,{form:R,layout:"vertical",children:a.jsx(b.Item,{label:"新密码",name:"password",tooltip:"至少 8 位,需包含大小写字母和数字",rules:[{required:!0,message:"请输入新密码"},{min:8,message:"密码至少 8 位"},{pattern:/^[\x21-\x7e]+$/,message:"只能包含英文字母、数字和英文特殊字符"},{pattern:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/,message:"必须包含大小写字母和数字"}],children:a.jsx(ve.Password,{placeholder:"请输入新密码"})})})})]})},qE=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(1),[o,l]=g.useState(20),[c,d]=g.useState(0),[u,h]=g.useState({}),[m]=b.useForm(),[_,x]=g.useState(!1),[f,p]=g.useState(null),[I]=b.useForm(),S=async(F=s,P=o,V=u)=>{t(!0);try{const X=await Ya.list({...V,page:F,page_size:P});(X.code===0||X.code===200)&&X.data?(n(X.data.items),d(X.data.total),i(X.data.page),l(X.data.size)):E.error(X.message||"获取角色列表失败")}catch(X){E.error(X.message||"获取角色列表失败")}finally{t(!1)}};g.useEffect(()=>{S(1,o,u)},[]);const w=F=>{const P={keyword:F.keyword};h(P),S(1,o,P)},v=()=>{p(null),I.resetFields(),I.setFieldsValue({permissions:[]}),x(!0)},y=F=>{p(F),I.setFieldsValue({name:F.name,description:F.description,permissions:Fs(F.permissions)}),x(!0)},R=async()=>{try{const F=await I.validateFields(),P=nc(F.permissions||[]),V=f?await Ya.update(f.id,{name:F.name,description:F.description,permissions:P}):await Ya.create({name:F.name,description:F.description,permissions:P});V.code===0||V.code===200?(E.success(f?"角色已更新":"角色已创建"),x(!1),S()):E.error(V.message||"操作失败")}catch(F){if(F!=null&&F.errorFields)return;E.error(F.message||"操作失败")}},j=F=>{Me.confirm({title:"确认删除该角色?",content:"删除角色会影响已绑定的管理员权限,请谨慎操作。",okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const P=await Ya.delete(F.id);P.code===0||P.code===200?(E.success("角色已删除"),S()):E.error(P.message||"删除失败")}catch(P){E.error(P.message||"删除失败")}}})},T=F=>{const P=Fs(F);return P.length?a.jsx(Ie,{wrap:!0,children:P.map(V=>{var _e;const X=((_e=ks.find(B=>B.value===V))==null?void 0:_e.label)||V;return a.jsx(Te,{color:"blue",children:X},V)})}):"--"},C=[{title:"ID",dataIndex:"id",width:80},{title:"角色名称",dataIndex:"name",width:160},{title:"描述",dataIndex:"description",width:200,render:F=>F||"--"},{title:"权限",dataIndex:"permissions",width:320,render:(F,P)=>T(P.permissions)},{title:"创建时间",dataIndex:"created_at",width:180,render:F=>Ke(F)},{title:"操作",key:"actions",width:160,render:(F,P)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>y(P),children:"编辑"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>j(P),children:"删除"})]})}];return a.jsxs("div",{className:"admin-roles",children:[a.jsx(be,{className:"admin-roles-filter",children:a.jsxs(b,{form:m,layout:"inline",onFinish:w,children:[a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"角色名称",allowClear:!0})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>m.resetFields(),children:"重置"}),a.jsx(Y,{onClick:v,children:"创建角色"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:C,dataSource:r,loading:e,scroll:{x:1e3},pagination:{current:s,pageSize:o,total:c,showSizeChanger:!0,onChange:(F,P)=>{i(F),l(P),S(F,P)}}})}),a.jsx(Me,{title:f?"编辑角色":"创建角色",open:_,onCancel:()=>x(!1),onOk:R,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:I,layout:"vertical",children:[a.jsx(b.Item,{label:"角色名称",name:"name",rules:[{required:!0,message:"请输入角色名称"}],children:a.jsx(ve,{placeholder:"角色名称"})}),a.jsx(b.Item,{label:"描述",name:"description",children:a.jsx(ve.TextArea,{placeholder:"可选描述",rows:3})}),a.jsx(b.Item,{label:"权限",name:"permissions",rules:[{required:!0,message:"请选择权限"}],children:a.jsx(gl.Group,{options:ks})})]})})]})},{Text:XE}=Cr,JE=[{label:"启用",value:"active"},{label:"已使用",value:"used"},{label:"已删除",value:"deleted"},{label:"禁用",value:"disabled"},{label:"已过期",value:"expired"}],ZE=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,{renderTenantId:r}=Yr(),[n,s]=g.useState(!1),[i,o]=g.useState([]),[l,c]=g.useState(1),[d,u]=g.useState(20),[h,m]=g.useState(0),[_,x]=g.useState({}),[f]=b.useForm(),[p,I]=g.useState(!1),[S,w]=g.useState(!1),[v]=b.useForm(),[y,R]=g.useState([]),[j,T]=g.useState(!1),[C,F]=g.useState(!1),[P,V]=g.useState(!1),[X,_e]=g.useState([]),[B,te]=g.useState(0),[J,G]=g.useState(1),[se,Q]=g.useState(10),[de,k]=g.useState(null),W=b.useWatch("tenant_id",v),z=b.useWatch("auto_add_friend",v),D=async(le=l,fe=d,Re=_)=>{s(!0);try{const Ue=await qa.list({...Re,page:le,page_size:fe});(Ue.code===0||Ue.code===200)&&Ue.data?(o(Ue.data.items),m(Ue.data.total),c(Ue.data.page),u(Ue.data.size)):E.error(Ue.message||"获取邀请码列表失败")}catch(Ue){E.error(Ue.message||"获取邀请码列表失败")}finally{s(!1)}},q=g.useCallback(async(le,fe)=>{T(!0);try{const Re=await Lt.list({tenant_id:le,keyword:fe||void 0,status:"active",page:1,page_size:100});(Re.code===0||Re.code===200)&&Re.data&&R(Re.data.items.map(nt=>({value:nt.id,label:`${nt.nickname||nt.username} (${nt.username})`})))}catch{R([])}finally{T(!1)}},[]);g.useEffect(()=>{D(1,d,_)},[]),g.useEffect(()=>{if(!p)return;const le=t?W:e==null?void 0:e.tenant_id;le?q(le):R([])},[p,W,t,e==null?void 0:e.tenant_id,q]);const K=le=>{const fe={tenant_id:le.tenant_id,status:le.status,keyword:le.keyword};x(fe),D(1,d,fe)},Z=()=>{v.resetFields(),v.setFieldsValue({no_expire:!1,auto_add_friend:!1}),!t&&(e!=null&&e.tenant_id)&&v.setFieldsValue({tenant_id:e.tenant_id}),I(!0)},me=async()=>{try{const le=await v.validateFields(),fe=t?le.tenant_id:e==null?void 0:e.tenant_id;if(!fe){E.error("无法获取租户信息");return}if(le.auto_add_friend&&!le.bind_user_id){E.error("开启自动加好友时必须指定用户");return}w(!0);const Re=await qa.create({tenant_id:fe,bind_user_id:le.bind_user_id,no_expire:le.no_expire,auto_add_friend:le.auto_add_friend});Re.code===0||Re.code===200?(E.success("邀请码已生成"),I(!1),D(1,d,_)):E.error(Re.message||"创建失败")}catch(le){if(le!=null&&le.errorFields)return;E.error(le.message||"创建失败")}finally{w(!1)}},ie=async(le,fe=1,Re=se)=>{V(!0);try{const Ue=await qa.listUsages(le.id,{page:fe,page_size:Re});(Ue.code===0||Ue.code===200)&&Ue.data?(_e(Ue.data.items),te(Ue.data.total),G(Ue.data.page),Q(Ue.data.size)):E.error(Ue.message||"获取注册用户失败")}catch(Ue){E.error(Ue.message||"获取注册用户失败")}finally{V(!1)}},he=le=>{k(le),F(!0),ie(le,1,se)},we=le=>{Me.confirm({title:"确认标记删除该邀请码?",content:"删除后不可再用于注册,历史记录仍保留在列表中。",okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const fe=await qa.delete(le.id);fe.code===0||fe.code===200?(E.success("邀请码已标记删除"),D()):E.error(fe.message||"删除失败")}catch(fe){E.error(fe.message||"删除失败")}}})},He=le=>le==="active"?a.jsx(Te,{color:"green",children:"启用"}):le==="used"?a.jsx(Te,{color:"blue",children:"已使用"}):le==="expired"?a.jsx(Te,{color:"orange",children:"已过期"}):le==="deleted"?a.jsx(Te,{children:"已删除"}):a.jsx(Te,{color:"red",children:"禁用"}),ze=[{title:"ID",dataIndex:"id",key:"id",width:80},{title:"租户",dataIndex:"tenant_id",key:"tenant_id",width:140,render:r},{title:"邀请码",dataIndex:"code",width:100},{title:"指定用户",dataIndex:"bind_username",width:140,render:(le,fe)=>le?`${le}${fe.bind_user_id?` (#${fe.bind_user_id})`:""}`:"-"},{title:"自动加好友",dataIndex:"auto_add_friend",width:110,render:le=>le?a.jsx(Te,{color:"cyan",children:"是"}):a.jsx(Te,{children:"否"})},{title:"使用人数",dataIndex:"used_count",width:90,render:le=>le??0},{title:"创建人",dataIndex:"created_by",width:90},{title:"状态",dataIndex:"status",width:90,render:le=>He(le)},{title:"过期时间",dataIndex:"expires_at",width:170,render:le=>le?Ke(le):"永久有效"},{title:"创建时间",dataIndex:"created_at",width:170,render:le=>Ke(le)},{title:"操作",key:"actions",width:150,fixed:"right",render:(le,fe)=>a.jsxs(Ie,{size:"small",children:[a.jsx(Y,{size:"small",onClick:()=>he(fe),children:"查看"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>we(fe),disabled:fe.status==="deleted",children:"删除"})]})}],gt=t?ze:ze.filter(le=>le.key!=="id"&&le.key!=="tenant_id"),qe=[{title:"用户ID",dataIndex:"user_id",width:100},{title:"用户名",dataIndex:"username",width:140},{title:"昵称",dataIndex:"nickname",width:140,render:le=>le||"-"},{title:"注册时间",dataIndex:"used_at",width:180,render:le=>Ke(le)}];return a.jsxs("div",{className:"admin-invite-codes",children:[a.jsx(be,{className:"admin-invite-codes-filter",children:a.jsxs(b,{form:f,layout:"inline",onFinish:K,children:[t&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:JE,style:{width:120}})}),a.jsx(b.Item,{label:"邀请码",name:"keyword",children:a.jsx(ve,{placeholder:"6 位数字",allowClear:!0,maxLength:6})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>f.resetFields(),children:"重置"}),a.jsx(Y,{type:"primary",onClick:Z,children:"生成邀请码"})]})})]})}),a.jsxs(be,{children:[a.jsx(XE,{type:"secondary",style:{display:"block",marginBottom:12},children:"每次生成 1 个邀请码,可多次使用。可指定企业与用户;勾选「不限制时间」则永久有效;开启「自动加好友」后,使用该码注册的用户将直接成为指定用户的好友。点击「查看」可查看使用该邀请码注册的用户列表。"}),a.jsx(Ye,{rowKey:"id",columns:gt,dataSource:i,loading:n,scroll:{x:1500},pagination:{current:l,pageSize:d,total:h,showSizeChanger:!0,onChange:(le,fe)=>{c(le),u(fe),D(le,fe)}}})]}),a.jsx(Me,{title:"生成邀请码",open:p,onCancel:()=>I(!1),onOk:me,okText:"生成",cancelText:"取消",confirmLoading:S,destroyOnClose:!0,width:520,children:a.jsxs(b,{form:v,layout:"vertical",initialValues:{no_expire:!1,auto_add_friend:!1},children:[t?a.jsx(b.Item,{label:"企业",name:"tenant_id",rules:[{required:!0,message:"请选择企业"}],children:a.jsx(ot,{placeholder:"请选择企业",style:{width:"100%"}})}):a.jsx(b.Item,{label:"企业",children:a.jsx(ve,{value:r(e==null?void 0:e.tenant_id),disabled:!0})}),a.jsx(b.Item,{label:"指定用户",name:"bind_user_id",rules:z?[{required:!0,message:"开启自动加好友时必须指定用户"}]:[],extra:"可选;开启自动加好友时必填",children:a.jsx($e,{allowClear:!0,showSearch:!0,placeholder:"搜索用户名或昵称",optionFilterProp:"label",loading:j,options:y,onSearch:le=>{const fe=t?W:e==null?void 0:e.tenant_id;fe&&q(fe,le)},disabled:!(t?W:e!=null&&e.tenant_id)})}),a.jsx(b.Item,{label:"不限制时间",name:"no_expire",valuePropName:"checked",extra:"不勾选时默认 24 小时有效",children:a.jsx(rt,{checkedChildren:"永久",unCheckedChildren:"24小时"})}),a.jsx(b.Item,{label:"自动加好友",name:"auto_add_friend",valuePropName:"checked",extra:"开启后,使用该邀请码注册的用户将直接成为「指定用户」的好友",children:a.jsx(rt,{checkedChildren:"开启",unCheckedChildren:"关闭"})})]})}),a.jsx(Me,{title:de?`注册用户 - 邀请码 ${de.code}`:"注册用户",open:C,onCancel:()=>{F(!1),k(null)},footer:null,width:720,destroyOnClose:!0,children:a.jsx(Ye,{rowKey:le=>`${le.user_id}-${le.used_at}`,columns:qe,dataSource:X,loading:P,pagination:{current:J,pageSize:se,total:B,showSizeChanger:!0,onChange:(le,fe)=>{de&&ie(de,le,fe)}}})})]})},eu=[{label:"草稿",value:"draft"},{label:"已发布",value:"published"}],QE=()=>{const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),{renderTenantId:n}=Yr(),[s,i]=g.useState(!1),[o,l]=g.useState([]),[c,d]=g.useState(1),[u,h]=g.useState(20),[m,_]=g.useState(0),[x,f]=g.useState({}),[p]=b.useForm(),[I,S]=g.useState(!1),[w,v]=g.useState(null),[y]=b.useForm(),R=async(B=c,te=u,J=x)=>{i(!0);try{const G=await zs.list({...J,page:B,page_size:te});(G.code===0||G.code===200)&&G.data?(l(G.data.items),_(G.data.total),d(G.data.page),h(G.data.size)):E.error(G.message||"获取公告列表失败")}catch(G){E.error(G.message||"获取公告列表失败")}finally{i(!1)}};g.useEffect(()=>{R(1,u,x)},[]);const j=B=>{const te={tenant_id:B.tenant_id,status:B.status,keyword:B.keyword};f(te),R(1,u,te)},T=()=>{v(null),y.resetFields(),y.setFieldsValue({status:"draft"}),S(!0)},C=B=>{v(B),y.setFieldsValue({tenant_id:B.tenant_id??void 0,title:B.title,content:B.content,status:B.status}),S(!0)},F=async()=>{try{const B=await y.validateFields(),te={tenant_id:B.tenant_id||void 0,title:B.title,content:B.content,status:B.status},J=w?await zs.update(w.id,te):await zs.create(te);J.code===0||J.code===200?(E.success(w?"公告已更新":"公告已创建"),S(!1),R()):E.error(J.message||"操作失败")}catch(B){if(B!=null&&B.errorFields)return;E.error(B.message||"操作失败")}},P=B=>{Me.confirm({title:"确认发布该公告?",okText:"发布",cancelText:"取消",onOk:async()=>{try{const te=await zs.publish(B.id);te.code===0||te.code===200?(E.success("公告已发布"),R()):E.error(te.message||"发布失败")}catch(te){E.error(te.message||"发布失败")}}})},V=B=>{Me.confirm({title:"确认删除该公告?",okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const te=await zs.delete(B.id);te.code===0||te.code===200?(E.success("公告已删除"),R()):E.error(te.message||"删除失败")}catch(te){E.error(te.message||"删除失败")}}})},X=B=>B==="published"?a.jsx(Te,{color:"green",children:"已发布"}):a.jsx(Te,{color:"orange",children:"草稿"}),_e=[{title:"ID",dataIndex:"id",width:80},{title:"租户",dataIndex:"tenant_id",width:160,render:B=>B?n(B):"全局"},{title:"标题",dataIndex:"title",width:200},{title:"状态",dataIndex:"status",width:100,render:B=>X(B)},{title:"发布时间",dataIndex:"published_at",width:180,render:B=>Ke(B)},{title:"创建时间",dataIndex:"created_at",width:180,render:B=>Ke(B)},{title:"操作",key:"actions",width:220,render:(B,te)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>C(te),children:"编辑"}),te.status!=="published"&&a.jsx(Y,{size:"small",onClick:()=>P(te),children:"发布"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>V(te),children:"删除"})]})}];return a.jsxs("div",{className:"admin-announcements",children:[a.jsx(be,{className:"admin-announcements-filter",children:a.jsxs(b,{form:p,layout:"inline",onFinish:j,children:[r&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:eu,style:{width:120}})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"公告标题",allowClear:!0})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>p.resetFields(),children:"重置"}),a.jsx(Y,{onClick:T,children:"创建公告"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:_e,dataSource:o,loading:s,scroll:{x:1200},pagination:{current:c,pageSize:u,total:m,showSizeChanger:!0,onChange:(B,te)=>{d(B),h(te),R(B,te)}}})}),a.jsx(Me,{title:w?"编辑公告":"创建公告",open:I,onCancel:()=>S(!1),onOk:F,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:y,layout:"vertical",children:[a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,placeholder:"为空表示全局公告",style:{width:"100%"}})}),a.jsx(b.Item,{label:"标题",name:"title",rules:[{required:!0,message:"请输入公告标题"}],children:a.jsx(ve,{placeholder:"公告标题"})}),a.jsx(b.Item,{label:"内容",name:"content",rules:[{required:!0,message:"请输入公告内容"}],children:a.jsx(ve.TextArea,{placeholder:"公告内容",rows:4})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{options:eu})})]})})]})},Ho=[{label:"低",value:"low"},{label:"中",value:"medium"},{label:"高",value:"high"}],Vo=[{label:"启用",value:"active"},{label:"禁用",value:"disabled"}],ej=()=>{const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),{renderTenantId:n}=Yr(),[s,i]=g.useState(!1),[o,l]=g.useState([]),[c,d]=g.useState(1),[u,h]=g.useState(20),[m,_]=g.useState(0),[x,f]=g.useState({}),[p]=b.useForm(),[I,S]=g.useState(!1),[w,v]=g.useState(!1),[y,R]=g.useState(null),[j]=b.useForm(),[T]=b.useForm(),C=async(D=c,q=u,K=x)=>{i(!0);try{const Z=await ls.list({...K,page:D,page_size:q});(Z.code===0||Z.code===200)&&Z.data?(l(Z.data.items),_(Z.data.total),d(Z.data.page),h(Z.data.size)):E.error(Z.message||"获取敏感词列表失败")}catch(Z){E.error(Z.message||"获取敏感词列表失败")}finally{i(!1)}};g.useEffect(()=>{C(1,u,x)},[]);const F=D=>{const q={tenant_id:D.tenant_id,status:D.status,level:D.level,keyword:D.keyword};f(q),C(1,u,q)},P=()=>{R(null),j.resetFields(),j.setFieldsValue({level:"medium",status:"active"}),S(!0)},V=D=>{R(D),j.setFieldsValue({tenant_id:D.tenant_id??void 0,word:D.word,level:D.level,replacement:D.replacement,status:D.status}),S(!0)},X=async()=>{try{const D=await j.validateFields(),q={tenant_id:D.tenant_id||void 0,word:D.word,level:D.level,replacement:D.replacement,status:D.status},K=y?await ls.update(y.id,q):await ls.create(q);K.code===0||K.code===200?(E.success(y?"敏感词已更新":"敏感词已创建"),S(!1),C()):E.error(K.message||"操作失败")}catch(D){if(D!=null&&D.errorFields)return;E.error(D.message||"操作失败")}},_e=()=>{T.resetFields(),T.setFieldsValue({level:"medium",status:"active"}),v(!0)},B=D=>D.split(/[\n,,]/).map(q=>q.trim()).filter(Boolean),te=async()=>{var D,q;try{const K=await T.validateFields(),Z=B(K.words||"");if(Z.length===0){E.error("请输入敏感词列表");return}const me=await ls.import({tenant_id:K.tenant_id||void 0,words:Z,level:K.level,replacement:K.replacement,status:K.status});if(me.code===0||me.code===200){const he=((D=me.data)==null?void 0:D.created)??0,we=((q=me.data)==null?void 0:q.skipped)??0;E.success(`敏感词已导入,新增 ${he},跳过 ${we}`),v(!1),C()}else E.error(me.message||"导入失败")}catch(K){if(K!=null&&K.errorFields)return;E.error(K.message||"导入失败")}},J=async()=>{try{await ls.export(x),E.success("导出成功")}catch(D){E.error(D.message||"导出失败")}},G=g.useRef(null),se=()=>{var D;(D=G.current)==null||D.click()},Q=async D=>{var me;const q=(me=D.target.files)==null?void 0:me[0];if(!q)return;D.target.value="";const Z=(await q.text()).split(/[\n\r]+/).map(ie=>{var we;return(we=ie.split(",")[0])==null?void 0:we.trim().replace(/^"|"$/g,"")}).filter(ie=>ie&&ie!=="敏感词");if(Z.length===0){E.error("文件中未找到有效敏感词");return}T.resetFields(),T.setFieldsValue({level:"medium",status:"active",words:Z.join(` +`)}),v(!0)},de=D=>{Me.confirm({title:"确认删除该敏感词?",okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const q=await ls.delete(D.id);q.code===0||q.code===200?(E.success("敏感词已删除"),C()):E.error(q.message||"删除失败")}catch(q){E.error(q.message||"删除失败")}}})},k=D=>D==="high"?a.jsx(Te,{color:"red",children:"高"}):D==="low"?a.jsx(Te,{color:"blue",children:"低"}):a.jsx(Te,{color:"orange",children:"中"}),W=D=>D==="active"?a.jsx(Te,{color:"green",children:"启用"}):a.jsx(Te,{color:"red",children:"禁用"}),z=[{title:"ID",dataIndex:"id",width:80},...r?[{title:"私域",dataIndex:"tenant_id",width:180,render:D=>n(D)}]:[],{title:"敏感词",dataIndex:"word",width:180},{title:"等级",dataIndex:"level",width:100,render:D=>k(D)},{title:"替换词",dataIndex:"replacement",width:160,render:D=>D||"--"},{title:"状态",dataIndex:"status",width:100,render:D=>W(D)},{title:"创建时间",dataIndex:"created_at",width:180,render:D=>Ke(D)},{title:"操作",key:"actions",width:160,render:(D,q)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>V(q),children:"编辑"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>de(q),children:"删除"})]})}];return a.jsxs("div",{className:"admin-sensitive-words",children:[a.jsx(be,{className:"admin-sensitive-words-filter",children:a.jsxs(b,{form:p,layout:"inline",onFinish:F,children:[r&&a.jsx(b.Item,{label:"私域",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,placeholder:"请选择私域",style:{width:200}})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:Vo,style:{width:120}})}),a.jsx(b.Item,{label:"等级",name:"level",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:Ho,style:{width:120}})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"敏感词",allowClear:!0})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>p.resetFields(),children:"重置"}),a.jsx(Y,{onClick:P,children:"新增敏感词"}),a.jsx(Y,{onClick:_e,children:"批量导入"}),a.jsx(Y,{icon:a.jsx(Iu,{}),onClick:se,children:"文件导入"}),a.jsx(Y,{icon:a.jsx($r,{}),onClick:J,children:"批量导出"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:z,dataSource:o,loading:s,scroll:{x:1200},pagination:{current:c,pageSize:u,total:m,showSizeChanger:!0,onChange:(D,q)=>{d(D),h(q),C(D,q)}}})}),a.jsx(Me,{title:y?"编辑敏感词":"新增敏感词",open:I,onCancel:()=>S(!1),onOk:X,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:j,layout:"vertical",children:[r&&a.jsx(b.Item,{label:"私域",name:"tenant_id",rules:[{required:!0,message:"请选择私域"}],children:a.jsx(ot,{placeholder:"请选择私域",style:{width:"100%"}})}),a.jsx(b.Item,{label:"敏感词",name:"word",rules:[{required:!0,message:"请输入敏感词"}],children:a.jsx(ve,{placeholder:"敏感词"})}),a.jsx(b.Item,{label:"等级",name:"level",children:a.jsx($e,{options:Ho})}),a.jsx(b.Item,{label:"替换词",name:"replacement",children:a.jsx(ve,{placeholder:"可选替换词"})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{options:Vo})})]})}),a.jsx(Me,{title:"批量导入敏感词",open:w,onCancel:()=>v(!1),onOk:te,okText:"导入",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:T,layout:"vertical",children:[r&&a.jsx(b.Item,{label:"私域",name:"tenant_id",rules:[{required:!0,message:"请选择私域"}],children:a.jsx(ot,{placeholder:"请选择私域",style:{width:"100%"}})}),a.jsx(b.Item,{label:"等级",name:"level",children:a.jsx($e,{options:Ho})}),a.jsx(b.Item,{label:"替换词",name:"replacement",children:a.jsx(ve,{placeholder:"可选替换词"})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{options:Vo})}),a.jsx(b.Item,{label:"敏感词列表",name:"words",rules:[{required:!0,message:"请输入敏感词列表"}],children:a.jsx(ve.TextArea,{placeholder:"一行一个词,或使用英文逗号分隔",rows:5})})]})}),a.jsx("input",{ref:G,type:"file",accept:".txt,.csv",style:{display:"none"},onChange:Q})]})},tj=[{label:"群主",value:"owner"},{label:"管理员",value:"admin"},{label:"成员",value:"member"}],rj=()=>{const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),[n]=b.useForm(),[s,i]=g.useState(!1),[o,l]=g.useState([]),[c,d]=g.useState(1),[u,h]=g.useState(20),[m,_]=g.useState(0),[x,f]=g.useState(null),[p,I]=g.useState({}),S=async(C=c,F=u,P=p,V=x)=>{if(!V){l([]),_(0);return}i(!0);try{const X=await Xa.list(V,{...P,page:C,page_size:F});(X.code===0||X.code===200)&&X.data?(l(X.data.list),_(X.data.total),d(C),h(F)):E.error(X.message||"获取群成员列表失败")}catch(X){E.error(X.message||"获取群成员列表失败")}finally{i(!1)}};g.useEffect(()=>{x&&S(1,u,p,x)},[]);const w=async C=>{const F={keyword:C.keyword,role:C.role};if(r){if(!C.group_id){E.error("请输入群组 ID");return}f(C.group_id),I(F),S(1,u,F,C.group_id)}else{if(!C.group_code){E.error("请输入群号");return}try{const P=await Wn.list({group_code:C.group_code,page:1,page_size:1});if((P.code===0||P.code===200)&&P.data&&P.data.items&&P.data.items.length>0){const X=P.data.items[0].id;f(X),I(F),S(1,u,F,X)}else E.error("未找到该群号")}catch{E.error("查询群组失败")}}},v=C=>{if(!x){E.error("请先查询群组");return}Me.confirm({title:"确认踢出该成员?",okText:"踢出",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const F=await Xa.kick(x,C.user_id);F.code===0||F.code===200?(E.success("成员已踢出"),S()):E.error(F.message||"踢出失败")}catch(F){E.error(F.message||"踢出失败")}}})},y=C=>{if(!x){E.error("请先查询群组");return}Me.confirm({title:"确认禁言该成员?",content:`将永久禁言 ${C.nickname||C.username},直到手动解除。`,okText:"禁言",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const F=await Xa.mute(x,C.user_id);F.code===0||F.code===200?(E.success("成员已禁言"),S()):E.error(F.message||"禁言失败")}catch(F){E.error(F.message||"禁言失败")}}})},R=async C=>{if(!x){E.error("请先查询群组");return}try{const F=await Xa.unmute(x,C.user_id);F.code===0||F.code===200?(E.success("禁言已解除"),S()):E.error(F.message||"解除禁言失败")}catch(F){E.error(F.message||"解除禁言失败")}},j=C=>C==="owner"?a.jsx(Te,{color:"gold",children:"群主"}):C==="admin"?a.jsx(Te,{color:"blue",children:"管理员"}):a.jsx(Te,{children:"成员"}),T=[...r?[{title:"用户 ID",dataIndex:"user_id",width:100}]:[{title:"编号",dataIndex:"user_no",width:100}],{title:"用户名",dataIndex:"username",width:160},{title:"昵称",dataIndex:"nickname",width:160,render:(C,F)=>C||F.username},{title:"群昵称",dataIndex:"nickname_in_group",width:160,render:C=>C||"--"},{title:"角色",dataIndex:"role",width:120,render:C=>j(C)},{title:"禁言状态",dataIndex:"is_muted",width:120,render:C=>a.jsx(Te,{color:C?"red":"green",children:C?"禁言中":"正常"})},{title:"加入时间",dataIndex:"joined_at",width:180,render:C=>Ke(C)},{title:"操作",key:"actions",width:200,render:(C,F)=>{const P=F.role==="owner";return a.jsxs(Ie,{children:[F.is_muted?a.jsx(Y,{size:"small",disabled:P,onClick:()=>R(F),children:"解除禁言"}):a.jsx(Y,{size:"small",disabled:P,onClick:()=>y(F),children:"禁言"}),a.jsx(Y,{size:"small",danger:!0,disabled:P,onClick:()=>v(F),children:"踢出"})]})}}];return a.jsxs("div",{className:"admin-group-members",children:[a.jsx(be,{className:"admin-group-members-filter",children:a.jsxs(b,{form:n,layout:"inline",onFinish:w,children:[r?a.jsx(b.Item,{label:"群组 ID",name:"group_id",rules:[{required:!0,message:"请输入群组 ID"}],children:a.jsx(at,{placeholder:"必填",min:1})}):a.jsx(b.Item,{label:"群号",name:"group_code",rules:[{required:!0,message:"请输入群号"}],children:a.jsx(ve,{placeholder:"必填",allowClear:!0,style:{width:140}})}),a.jsx(b.Item,{label:"角色",name:"role",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:tj,style:{width:120}})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"用户名/昵称",allowClear:!0})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>n.resetFields(),children:"重置"}),a.jsx(Y,{onClick:()=>S(),children:"刷新"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"user_id",columns:T,dataSource:o,loading:s,scroll:{x:1200},pagination:{current:c,pageSize:u,total:m,showSizeChanger:!0,onChange:(C,F)=>{d(C),h(F),S(C,F)}}})})]})},nj=[{label:"活跃",value:"active"},{label:"排空中",value:"draining"},{label:"已禁用",value:"disabled"}],sj={active:"green",draining:"orange",disabled:"red"},aj={active:"活跃",draining:"排空中",disabled:"已禁用"},ij=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(1),[o,l]=g.useState(20),[c,d]=g.useState(0),[u,h]=g.useState(!1),[m,_]=g.useState(null),[x]=b.useForm(),f=async(y=s,R=o)=>{t(!0);try{const j=await On.list({page:y,page_size:R});if((j.code===0||j.code===200)&&j.data){const C=(j.data.items||[]).sort((F,P)=>F.is_default===P.is_default?0:F.is_default?-1:1);n(C),d(j.data.total),i(j.data.page),l(j.data.size)}else E.error(j.message||"获取节点列表失败")}catch(j){E.error(j.message||"获取节点列表失败")}finally{t(!1)}};g.useEffect(()=>{f(1,o)},[]);const p=()=>{_(null),x.resetFields(),h(!0)},I=y=>{_(y),x.setFieldsValue({name:y.name,base_url:y.base_url,status:y.status,version:y.version}),h(!0)},S=async()=>{try{const y=await x.validateFields(),R=m?await On.update(m.id,{name:y.name,base_url:y.base_url,status:y.status,version:y.version||void 0}):await On.create({node_id:y.node_id,name:y.name,base_url:y.base_url,version:y.version||void 0});R.code===0||R.code===200?(E.success(m?"节点已更新":"节点已创建"),h(!1),f()):E.error(R.message||"操作失败")}catch(y){if(y!=null&&y.errorFields)return;E.error(y.message||"操作失败")}},w=y=>{Me.confirm({title:"确认删除该节点?",content:y.tenant_count>0?"该节点下仍有绑定的租户,请先解绑后再删除。":"删除后将无法恢复,请谨慎操作。",okText:"删除",okButtonProps:{danger:!0,disabled:y.tenant_count>0},cancelText:"取消",onOk:async()=>{try{const R=await On.delete(y.id);R.code===0||R.code===200?(E.success("节点已删除"),f()):E.error(R.message||"删除失败")}catch(R){E.error(R.message||"删除失败")}}})},v=[{title:"ID",dataIndex:"id",width:60},{title:"节点 ID",dataIndex:"node_id",width:140},{title:"名称",dataIndex:"name",width:140},{title:"Base URL",dataIndex:"base_url",width:200},{title:"版本",dataIndex:"version",width:100},{title:"状态",dataIndex:"status",width:100,render:y=>a.jsx(Te,{color:sj[y]||"default",children:aj[y]||y})},{title:"绑定租户数",dataIndex:"tenant_count",width:110},{title:"默认节点",dataIndex:"is_default",width:100,render:y=>y?a.jsx(Te,{color:"blue",children:"默认"}):"-"},{title:"最后心跳",dataIndex:"last_heartbeat_at",width:170,render:y=>y?Ke(y):"-"},{title:"创建时间",dataIndex:"created_at",width:170,render:y=>Ke(y)},{title:"操作",key:"actions",width:160,render:(y,R)=>R.is_default?a.jsx(Te,{children:"中心节点(不可操作)"}):a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>I(R),children:"编辑"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>w(R),children:"删除"})]})}];return a.jsxs(a.Fragment,{children:[a.jsx(be,{className:"admin-nodes-filter",children:a.jsxs(b,{layout:"inline",children:[a.jsx(b.Item,{children:a.jsx(Y,{type:"primary",onClick:p,children:"创建节点"})}),a.jsx(b.Item,{children:a.jsx(Y,{onClick:()=>f(1,o),children:"刷新"})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:v,dataSource:r,loading:e,scroll:{x:1400},pagination:{current:s,pageSize:o,total:c,showSizeChanger:!0,onChange:(y,R)=>{i(y),l(R),f(y,R)}}})}),a.jsx(Me,{title:m?"编辑节点":"创建节点",open:u,onCancel:()=>h(!1),onOk:S,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:x,layout:"vertical",children:[!m&&a.jsx(b.Item,{label:"节点 ID",name:"node_id",rules:[{required:!0,message:"请输入节点 ID"}],children:a.jsx(ve,{placeholder:"如 node-01"})}),a.jsx(b.Item,{label:"节点名称",name:"name",rules:[{required:!0,message:"请输入节点名称"}],children:a.jsx(ve,{placeholder:"如 华东节点"})}),a.jsx(b.Item,{label:"Base URL",name:"base_url",rules:[{required:!0,message:"请输入 Base URL"}],children:a.jsx(ve,{placeholder:"如 https://node-01.example.com"})}),m&&a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{options:nj})}),a.jsx(b.Item,{label:"版本",name:"version",children:a.jsx(ve,{placeholder:"可选,如 v1.0.0"})})]})})]})},oj=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(1),[o,l]=g.useState(20),[c,d]=g.useState(0),u=async(m=s,_=o)=>{t(!0);try{const x=await On.cpAuditLogs({page:m,page_size:_});(x.code===0||x.code===200)&&x.data?(n(x.data.items||[]),d(x.data.total),i(x.data.page),l(x.data.size)):E.error(x.message||"获取审计日志失败")}catch(x){E.error(x.message||"获取审计日志失败")}finally{t(!1)}};g.useEffect(()=>{u(1,o)},[]);const h=[{title:"ID",dataIndex:"id",width:60},{title:"操作人",dataIndex:"operator_name",width:120},{title:"操作",dataIndex:"action",width:140},{title:"目标类型",dataIndex:"target_type",width:120},{title:"目标 ID",dataIndex:"target_id",width:140},{title:"备注",key:"reason",width:200,render:(m,_)=>{var x;return((x=_.meta)==null?void 0:x.reason)||"-"}},{title:"时间",dataIndex:"created_at",width:170,render:m=>Ke(m)}];return a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:h,dataSource:r,loading:e,scroll:{x:900},pagination:{current:s,pageSize:o,total:c,showSizeChanger:!0,onChange:(m,_)=>{i(m),l(_),u(m,_)}}})})},lj=()=>{const e=[{key:"nodes",label:"节点管理",children:a.jsx(ij,{})},{key:"audit",label:"控制台操作日志",children:a.jsx(oj,{})}];return a.jsx("div",{className:"admin-nodes",children:a.jsx(be,{className:"admin-nodes-tabs",children:a.jsx(zi,{items:e})})})},{Text:xs}=Cr,tu=[{label:"成员加入",value:"member_joined"},{label:"成员退出",value:"member_left"},{label:"成员踢出",value:"member_kicked"},{label:"注销退出",value:"member_deactivated"},{label:"成员邀请",value:"member_invited"},{label:"成员禁言",value:"member_muted"},{label:"成员解禁",value:"member_unmuted"},{label:"设置管理员",value:"admin_set"},{label:"撤销管理员",value:"admin_removed"},{label:"转让群主",value:"owner_transfer"},{label:"全体禁言",value:"all_mute"},{label:"入群验证",value:"join_verify"},{label:"群私聊",value:"private_chat"},{label:"成员上传权限",value:"member_upload"},{label:"成员改昵称权限",value:"member_nickname"},{label:"修改群名",value:"name_changed"},{label:"修改头像",value:"avatar_changed"},{label:"修改简介",value:"desc_changed"},{label:"发布公告",value:"announcement_set"},{label:"群创建",value:"group_created"},{label:"群解散",value:"group_dismissed"},{label:"群封禁",value:"group_banned"},{label:"群解封",value:"group_unbanned"},{label:"上传文件",value:"file_uploaded"},{label:"删除文件",value:"file_deleted"},{label:"展示身份标识",value:"show_member_role"},{label:"撤回消息",value:"message_revoked"}],cj={member_joined:"green",member_left:"orange",member_kicked:"red",member_deactivated:"magenta",member_muted:"volcano",member_unmuted:"cyan",admin_set:"blue",admin_removed:"purple",owner_transfer:"gold",all_mute:"orange",all_mute_on:"orange",all_mute_off:"orange",join_verify:"orange",join_verify_on:"orange",join_verify_off:"orange",private_chat:"orange",private_chat_on:"orange",private_chat_off:"orange",member_upload:"orange",member_nickname:"orange",group_created:"green",group_dismissed:"red",group_banned:"red",group_unbanned:"green",file_uploaded:"cyan",file_deleted:"orange",show_member_role:"orange",message_revoked:"red"},dj={all_mute_on:"全体禁言",all_mute_off:"全体禁言",join_verify_on:"入群验证",join_verify_off:"入群验证",private_chat_on:"群私聊",private_chat_off:"群私聊"},ru={on:"开启",off:"关闭"},nu=new Set(["announcement_set","desc_changed","message_revoked"]),uj={group_banned:"封禁原因",message_revoked:"消息内容"},fj=()=>{const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),[n,s]=g.useState(!1),[i,o]=g.useState([]),[l,c]=g.useState(1),[d,u]=g.useState(20),[h,m]=g.useState(0),[_,x]=g.useState({}),[f]=b.useForm(),p=async(w=l,v=d,y=_)=>{s(!0);try{const R=await p_.list({...y,page:w,page_size:v});(R.code===0||R.code===200)&&R.data?(o(R.data.items),m(R.data.total),c(R.data.page),u(R.data.size)):E.error(R.message||"获取群事件失败")}catch(R){E.error(R.message||"获取群事件失败")}finally{s(!1)}};g.useEffect(()=>{p(1,d,_)},[]);const I=w=>{const v={};r?(w.group_id&&(v.group_id=w.group_id),w.tenant_id&&(v.tenant_id=w.tenant_id),w.operator_id&&(v.operator_id=w.operator_id),w.target_user_id&&(v.target_user_id=w.target_user_id)):(w.group_code&&(v.group_code=w.group_code.trim()),w.operator_username&&(v.operator_username=w.operator_username.trim()),w.target_username&&(v.target_username=w.target_username.trim())),w.event_type&&(v.event_type=w.event_type),w.startTime&&(v.start_time=w.startTime.format("YYYY-MM-DD HH:mm:ss")),w.endTime&&(v.end_time=w.endTime.format("YYYY-MM-DD HH:mm:ss")),x(v),p(1,d,v)},S=[...r?[{title:"ID",dataIndex:"id",width:80}]:[],{title:"群名称",dataIndex:"group_name",width:160,render:(w,v)=>a.jsxs("div",{children:[a.jsx("div",{children:v.group_name||"--"}),v.group_code&&a.jsx(xs,{type:"secondary",style:{fontSize:12},children:v.group_code})]})},{title:"事件类型",dataIndex:"event_type",width:130,render:w=>{var R;const v=dj[w]||((R=tu.find(j=>j.value===w))==null?void 0:R.label)||w,y=cj[w]||"default";return a.jsx(Te,{color:y,children:v})}},{title:"操作人",dataIndex:"operator_name",width:140,render:(w,v)=>{var y;if(!v.operator_name&&!v.operator_username)return"--";if((y=v.operator_name)!=null&&y.startsWith("管理员: ")){const R=v.operator_name.slice(5);return a.jsxs("div",{children:[a.jsx(Te,{color:"blue",style:{marginRight:0},children:"管理员"}),a.jsx("div",{children:a.jsx(xs,{type:"secondary",style:{fontSize:12},children:R})})]})}return a.jsxs("div",{children:[a.jsx("div",{children:v.operator_name||v.operator_username}),v.operator_username&&v.operator_name&&a.jsx(xs,{type:"secondary",style:{fontSize:12},children:v.operator_username})]})}},{title:"目标用户",dataIndex:"target_user_name",width:140,render:(w,v)=>!v.target_user_name&&!v.target_user_username?"--":a.jsxs("div",{children:[a.jsx("div",{children:v.target_user_name||v.target_user_username}),v.target_user_username&&v.target_user_name&&a.jsx(xs,{type:"secondary",style:{fontSize:12},children:v.target_user_username})]})},{title:"可见范围",dataIndex:"visibility",width:130,render:w=>{const v=w==="全员可见"?"green":"orange";return a.jsx(Te,{color:v,children:w})}},{title:"旧值",dataIndex:"old_value",width:140,ellipsis:!0,render:(w,v)=>{if(!w)return"--";if(v.event_type==="avatar_changed")return a.jsx(Mr,{src:w,width:32,height:32,style:{objectFit:"cover",borderRadius:4}});if(nu.has(v.event_type))return a.jsx(Y,{type:"link",size:"small",style:{padding:0,height:"auto",fontSize:12},onClick:()=>{Me.info({title:"旧值",width:520,okText:"关闭",content:a.jsx("div",{style:{maxHeight:400,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"},dangerouslySetInnerHTML:{__html:w}})})},children:"查看内容"});const y=ru[w]||w;return a.jsx(Xt,{title:y,children:a.jsx(xs,{ellipsis:!0,style:{maxWidth:120},children:y})})}},{title:"新值",dataIndex:"new_value",width:140,ellipsis:!0,render:(w,v)=>{if(!w)return"--";if(v.event_type==="avatar_changed")return a.jsx(Mr,{src:w,width:32,height:32,style:{objectFit:"cover",borderRadius:4}});if(nu.has(v.event_type)){const T=v.event_type==="message_revoked"?"消息内容":"新值";return a.jsx(Y,{type:"link",size:"small",style:{padding:0,height:"auto",fontSize:12},onClick:()=>{Me.info({title:T,width:520,okText:"关闭",content:a.jsx("div",{style:{maxHeight:400,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"},dangerouslySetInnerHTML:{__html:w}})})},children:"查看内容"})}const y=uj[v.event_type],R=ru[w]||w,j=y?`${y}:${R}`:R;return a.jsx(Xt,{title:j,children:a.jsx(xs,{ellipsis:!0,style:{maxWidth:120},children:j})})}},{title:"时间",dataIndex:"created_at",width:180,render:w=>Ke(w)}];return a.jsxs("div",{className:"admin-group-events",children:[a.jsx(be,{className:"admin-group-events-filter",children:a.jsxs(b,{form:f,layout:"inline",onFinish:I,children:[r?a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"群 ID",name:"group_id",children:a.jsx(at,{placeholder:"可选",min:1})}),a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"操作人 ID",name:"operator_id",children:a.jsx(at,{placeholder:"可选",min:1})}),a.jsx(b.Item,{label:"目标用户 ID",name:"target_user_id",children:a.jsx(at,{placeholder:"可选",min:1})})]}):a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"群号",name:"group_code",children:a.jsx(ve,{placeholder:"可选",allowClear:!0,style:{width:140}})}),a.jsx(b.Item,{label:"操作人",name:"operator_username",children:a.jsx(ve,{placeholder:"用户名",allowClear:!0,style:{width:140}})}),a.jsx(b.Item,{label:"目标用户",name:"target_username",children:a.jsx(ve,{placeholder:"用户名",allowClear:!0,style:{width:140}})})]}),a.jsx(b.Item,{label:"事件类型",name:"event_type",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:tu,style:{width:140}})}),a.jsx(b.Item,{label:"开始时间",name:"startTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{label:"结束时间",name:"endTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:()=>f.resetFields(),children:"重置"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:S,dataSource:i,loading:n,scroll:{x:1100},pagination:{current:l,pageSize:d,total:h,showSizeChanger:!0,onChange:(w,v)=>{c(w),u(v),p(w,v)}}})})]})},{Text:hj}=Cr,mj=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState(!1),[s,i]=g.useState([]),[o,l]=g.useState("random"),[c,d]=g.useState(!1),[u]=b.useForm(),h=async()=>{t(!0);try{const S=await Ao.get();(S.code===0||S.code===200)&&S.data&&(i(S.data.avatars||[]),l(S.data.strategy||"random"))}catch(S){E.error(S.message||"获取头像池失败")}finally{t(!1)}};g.useEffect(()=>{h()},[]);const m=async()=>{n(!0);try{const S=await Ao.update({avatars:s,strategy:o});S.code===0||S.code===200?E.success("保存成功"):E.error(S.message||"保存失败")}catch(S){E.error(S.message||"保存失败")}finally{n(!1)}},_=async()=>{try{const S=await Ao.backfill();(S.code===0||S.code===200)&&S.data?E.success(`补齐完成,更新了 ${S.data.updated} 个用户`):E.error(S.message||"补齐失败")}catch(S){E.error(S.message||"补齐失败")}},x=(S,w)=>{i(v=>v.map(y=>y.key===S?{...y,enabled:w}:y))},f=S=>{i(w=>w.filter(v=>v.key!==S))},p=S=>{if(s.some(v=>v.key===S.key)){E.error("Key 已存在");return}i(v=>[...v,{key:S.key,url:S.url,enabled:!0,sort_order:S.sort_order??v.length}]),d(!1),u.resetFields()},I=[{title:"预览",dataIndex:"url",width:80,render:S=>a.jsx("img",{src:S,alt:"avatar",className:"avatar-preview"})},{title:"Key",dataIndex:"key",width:140},{title:"URL",dataIndex:"url",width:300,render:S=>a.jsx(hj,{ellipsis:!0,style:{maxWidth:280},children:S})},{title:"排序",dataIndex:"sort_order",width:80},{title:"启用",dataIndex:"enabled",width:80,render:(S,w)=>a.jsx(rt,{checked:S,onChange:v=>x(w.key,v)})},{title:"操作",width:80,render:(S,w)=>a.jsx(js,{title:"确认删除?",onConfirm:()=>f(w.key),children:a.jsx(Y,{type:"link",danger:!0,size:"small",children:"删除"})})}];return a.jsxs("div",{className:"admin-avatar-pool",children:[a.jsx(be,{title:"默认头像池配置",extra:a.jsxs(Ie,{children:[a.jsx($e,{value:o,onChange:l,style:{width:120},options:[{label:"随机分配",value:"random"},{label:"顺序分配",value:"sequential"}]}),a.jsx(Y,{icon:a.jsx(ua,{}),onClick:()=>d(!0),children:"添加头像"}),a.jsx(Y,{type:"primary",icon:a.jsx(Js,{}),onClick:m,loading:r,children:"保存配置"}),a.jsx(js,{title:"为所有无头像用户补齐默认头像?",onConfirm:_,children:a.jsx(Y,{icon:a.jsx(rm,{}),children:"存量补齐"})})]}),children:a.jsx(Ye,{rowKey:"key",columns:I,dataSource:s,loading:e,pagination:!1,scroll:{x:800}})}),a.jsx(Me,{title:"添加头像",open:c,onCancel:()=>d(!1),onOk:()=>u.submit(),destroyOnClose:!0,children:a.jsxs(b,{form:u,layout:"vertical",onFinish:p,children:[a.jsx(b.Item,{label:"Key",name:"key",rules:[{required:!0,message:"请输入Key"}],children:a.jsx(ve,{placeholder:"如: avatar_01"})}),a.jsx(b.Item,{label:"图片 URL",name:"url",rules:[{required:!0,message:"请输入URL"}],children:a.jsx(ve,{placeholder:"https://..."})}),a.jsx(b.Item,{label:"排序",name:"sort_order",children:a.jsx(at,{min:0,placeholder:"0"})})]})})]})},xj=()=>{const{renderTenantId:e}=Yr(),[t,r]=g.useState(!1),[n,s]=g.useState([]),[i,o]=g.useState(1),[l,c]=g.useState(20),[d,u]=g.useState(0),[h,m]=g.useState({}),[_]=b.useForm(),[x,f]=g.useState(!1),[p,I]=g.useState(null),[S]=b.useForm(),[w,v]=g.useState(!1),[y]=b.useForm(),R=async(B=i,te=l,J=h)=>{r(!0);try{const G=await $s.list({...J,page:B,page_size:te});(G.code===0||G.code===200)&&G.data?(s(G.data.items),u(G.data.total),o(G.data.page),c(G.data.size)):E.error(G.message||"获取系统会话列表失败")}catch(G){E.error(G.message||"获取系统会话列表失败")}finally{r(!1)}};g.useEffect(()=>{R(1,l,h)},[]);const j=B=>{const te={};B.tenant_id&&(te.tenant_id=B.tenant_id),B.is_official!==void 0&&B.is_official!==null&&(te.is_official=B.is_official),B.keyword&&(te.keyword=B.keyword),m(te),R(1,l,te)},T=()=>{I(null),S.resetFields(),f(!0)},C=B=>{I(B),S.setFieldsValue({system_name:B.system_name,system_avatar:B.system_avatar,is_official:B.is_official}),f(!0)},F=async()=>{try{const B=await S.validateFields();let te;p?te=await $s.update(p.id,{system_name:B.system_name,system_avatar:B.system_avatar||void 0,is_official:B.is_official}):te=await $s.create({tenant_id:B.tenant_id,system_name:B.system_name,system_avatar:B.system_avatar||void 0,is_official:B.is_official}),te.code===0||te.code===200?(E.success(p?"系统会话已更新":"系统会话已创建"),f(!1),R()):E.error(te.message||"操作失败")}catch(B){if(B!=null&&B.errorFields)return;E.error(B.message||"操作失败")}},P=B=>{Me.confirm({title:"确认删除该系统会话?",content:`将删除系统会话「${B.system_name||"系统消息"}」及其关联数据,请谨慎操作。`,okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const te=await $s.delete(B.id);te.code===0||te.code===200?(E.success("系统会话已删除"),R()):E.error(te.message||"删除失败")}catch(te){E.error(te.message||"删除失败")}}})},V=()=>{y.resetFields(),v(!0)},X=async()=>{try{const B=await y.validateFields(),te={};B.type==="text"?te.text=B.text:te.url=B.url;const J=await $s.send({tenant_id:B.tenant_id,type:B.type,content:te});(J.code===0||J.code===200)&&J.data?(E.success(`发送成功:${J.data.sent_count} 人成功,${J.data.failed_count} 人失败`),v(!1)):E.error(J.message||"发送失败")}catch(B){if(B!=null&&B.errorFields)return;E.error(B.message||"发送失败")}},_e=[{title:"ID",dataIndex:"id",width:80},{title:"租户",dataIndex:"tenant_id",width:160,render:e},{title:"系统名称",dataIndex:"system_name",width:160},{title:"官方",dataIndex:"is_official",width:80,render:B=>a.jsx(Te,{color:B?"blue":"default",children:B?"是":"否"})},{title:"用户数",dataIndex:"user_count",width:100},{title:"最后消息时间",dataIndex:"last_msg_at",width:180,render:B=>B?Ke(B):"--"},{title:"创建时间",dataIndex:"created_at",width:180,render:B=>Ke(B)},{title:"操作",key:"actions",width:160,render:(B,te)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>C(te),children:"编辑"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>P(te),children:"删除"})]})}];return a.jsxs("div",{className:"admin-sys-conv",children:[a.jsx(be,{className:"admin-sys-conv-filter",children:a.jsxs(b,{form:_,layout:"inline",onFinish:j,children:[a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"官方",name:"is_official",children:a.jsxs($e,{placeholder:"全部",allowClear:!0,style:{width:100},children:[a.jsx($e.Option,{value:!0,children:"是"}),a.jsx($e.Option,{value:!1,children:"否"})]})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"系统名称",allowClear:!0})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>_.resetFields(),children:"重置"}),a.jsx(Y,{onClick:T,children:"创建会话"}),a.jsx(Y,{onClick:V,children:"发送消息"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:_e,dataSource:n,loading:t,scroll:{x:1e3},pagination:{current:i,pageSize:l,total:d,showSizeChanger:!0,onChange:(B,te)=>{o(B),c(te),R(B,te)}}})}),a.jsx(Me,{title:p?"编辑系统会话":"创建系统会话",open:x,onCancel:()=>f(!1),onOk:F,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:S,layout:"vertical",children:[!p&&a.jsx(b.Item,{label:"租户",name:"tenant_id",rules:[{required:!0,message:"请选择租户"}],children:a.jsx(ot,{style:{width:"100%"}})}),a.jsx(b.Item,{label:"系统名称",name:"system_name",children:a.jsx(ve,{placeholder:"默认:系统消息"})}),a.jsx(b.Item,{label:"头像 URL",name:"system_avatar",children:a.jsx(ve,{placeholder:"可选"})}),a.jsx(b.Item,{label:"官方标识",name:"is_official",initialValue:!1,children:a.jsxs($e,{children:[a.jsx($e.Option,{value:!0,children:"是"}),a.jsx($e.Option,{value:!1,children:"否"})]})})]})}),a.jsx(Me,{title:"发送系统消息",open:w,onCancel:()=>v(!1),onOk:X,okText:"发送",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:y,layout:"vertical",children:[a.jsx(b.Item,{label:"租户",name:"tenant_id",rules:[{required:!0,message:"请选择租户"}],children:a.jsx(ot,{style:{width:"100%"}})}),a.jsx(b.Item,{label:"消息类型",name:"type",rules:[{required:!0,message:"请选择消息类型"}],initialValue:"text",children:a.jsxs($e,{children:[a.jsx($e.Option,{value:"text",children:"文本"}),a.jsx($e.Option,{value:"image",children:"图片"})]})}),a.jsx(b.Item,{noStyle:!0,shouldUpdate:(B,te)=>B.type!==te.type,children:({getFieldValue:B})=>B("type")==="text"?a.jsx(b.Item,{label:"文本内容",name:"text",rules:[{required:!0,message:"请输入文本内容"}],children:a.jsx(ve.TextArea,{rows:4,placeholder:"消息内容"})}):a.jsx(b.Item,{label:"图片 URL",name:"url",rules:[{required:!0,message:"请输入图片 URL"}],children:a.jsx(ve,{placeholder:"图片链接"})})})]})})]})},pj=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(!1),[o,l]=g.useState(null),[c]=b.useForm(),d=async()=>{t(!0);try{const f=await ko.list();(f.code===0||f.code===200)&&f.data?n(f.data.items||[]):E.error(f.message||"获取系统通知配置失败")}catch(f){E.error(f.message||"获取系统通知配置失败")}finally{t(!1)}};g.useEffect(()=>{d()},[]);const u=()=>{l(null),c.resetFields(),c.setFieldsValue({enabled:!0}),i(!0)},h=f=>{l(f),c.setFieldsValue({enabled:f.enabled,content:f.content}),i(!0)},m=async()=>{try{const f=await c.validateFields(),p=await ko.update({enabled:f.enabled,content:f.content.trim()});p.code===0||p.code===200?(E.success(o?"系统通知已更新":"系统通知已配置"),i(!1),d()):E.error(p.message||"操作失败")}catch(f){if(f!=null&&f.errorFields)return;E.error(f.message||"操作失败")}},_=()=>{Me.confirm({title:"确认删除系统通知配置?",content:"删除后新注册用户将不再收到「系统消息」中的欢迎通知。",okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const f=await ko.delete();f.code===0||f.code===200?(E.success("已删除"),d()):E.error(f.message||"删除失败")}catch(f){E.error(f.message||"删除失败")}}})},x=[{title:"范围",dataIndex:"scope_label",width:120,render:f=>f||"全平台"},{title:"通知内容",dataIndex:"content",ellipsis:!0},{title:"状态",dataIndex:"enabled",width:100,render:f=>a.jsx(Te,{color:f?"green":"default",children:f?"启用":"停用"})},{title:"更新时间",dataIndex:"updated_at",width:180,render:f=>f?Ke(f):"-"},{title:"操作人",dataIndex:"updated_by_name",width:120,render:f=>f||"-"},{title:"操作",key:"action",width:160,render:(f,p)=>a.jsxs(Ie,{children:[a.jsx(Y,{type:"link",size:"small",onClick:()=>h(p),children:"编辑"}),a.jsx(Y,{type:"link",size:"small",danger:!0,onClick:_,children:"删除"})]})}];return a.jsxs("div",{className:"admin-system-notices",children:[a.jsxs(be,{title:"系统通知",extra:r.length===0?a.jsx(Y,{type:"primary",onClick:u,children:"配置系统通知"}):null,children:[a.jsx("p",{className:"page-desc",children:"配置全平台统一的新用户欢迎通知。启用后,用户注册成功时将通过「系统消息」会话收到该提示文案。"}),a.jsx(Ye,{rowKey:"scope",loading:e,columns:x,dataSource:r,pagination:!1,locale:{emptyText:"暂未配置,点击右上角按钮添加"}})]}),a.jsx(Me,{title:o?"编辑系统通知":"配置系统通知",open:s,onCancel:()=>i(!1),onOk:m,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:c,layout:"vertical",initialValues:{enabled:!0},children:[a.jsx(b.Item,{name:"enabled",label:"启用",valuePropName:"checked",children:a.jsx(rt,{checkedChildren:"启用",unCheckedChildren:"停用"})}),a.jsx(b.Item,{name:"content",label:"通知内容",rules:[{required:!0,message:"请输入通知内容"},{max:500,message:"最多 500 字"}],children:a.jsx(ve.TextArea,{rows:5,placeholder:"请输入新用户注册后在「系统消息」中展示的欢迎文案",showCount:!0,maxLength:500})})]})})]})},{Text:su}=Cr,gj=[{label:"用户",value:"user"},{label:"群组",value:"group"},{label:"消息",value:"message"}],_j=[{label:"待处理",value:"pending"},{label:"已处理",value:"resolved"},{label:"已驳回",value:"rejected"}],au={pending:"orange",resolved:"green",rejected:"red"},iu={pending:"待处理",resolved:"已处理",rejected:"已驳回"},ou={user:"用户",group:"群组",message:"消息"},vj=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,{renderTenantId:r}=Yr(),[n,s]=g.useState(!1),[i,o]=g.useState([]),[l,c]=g.useState(1),[d,u]=g.useState(20),[h,m]=g.useState(0),[_,x]=g.useState({}),[f]=b.useForm(),[p,I]=g.useState(!1),[S,w]=g.useState(!1),[v,y]=g.useState(null),[R,j]=g.useState(!1),[T,C]=g.useState(null),[F]=b.useForm(),P=async(J=l,G=d,se=_)=>{s(!0);try{const Q=await Fo.list({...se,page:J,page_size:G});(Q.code===0||Q.code===200)&&Q.data?(o(Q.data.items),m(Q.data.total),c(Q.data.page),u(Q.data.size)):E.error(Q.message||"获取举报列表失败")}catch(Q){E.error(Q.message||"获取举报列表失败")}finally{s(!1)}};g.useEffect(()=>{P(1,d,_)},[]);const V=J=>{const G={};J.tenant_id&&(G.tenant_id=J.tenant_id),J.target_type&&(G.target_type=J.target_type),J.status&&(G.status=J.status),x(G),P(1,d,G)},X=async J=>{y(null),I(!0),w(!0);try{const G=await Fo.detail(J);(G.code===0||G.code===200)&&G.data?y(G.data):E.error(G.message||"获取举报详情失败")}catch(G){E.error(G.message||"获取举报详情失败")}finally{w(!1)}},_e=J=>{C(J),F.resetFields(),j(!0)},B=async()=>{if(T)try{const J=await F.validateFields(),G=await Fo.handle(T.id,{status:J.status,handle_note:J.handle_note||void 0});G.code===0||G.code===200?(E.success("举报已处理"),j(!1),P()):E.error(G.message||"处理失败")}catch(J){if(J!=null&&J.errorFields)return;E.error(J.message||"处理失败")}},te=[...t?[{title:"ID",dataIndex:"id",width:80}]:[],...t?[{title:"租户",dataIndex:"tenant_id",width:160,render:r}]:[],{title:"举报人",dataIndex:"reporter_name",width:120},{title:"目标类型",dataIndex:"target_type",width:100,render:J=>a.jsx(Te,{color:"geekblue",children:ou[J]||J})},{title:"目标名称",dataIndex:"target_name",width:140},{title:"原因",dataIndex:"reason",width:200,render:J=>a.jsx(Xt,{title:J,children:a.jsx(su,{ellipsis:!0,style:{maxWidth:180},children:J})})},{title:"状态",dataIndex:"status",width:100,render:J=>a.jsx(Te,{color:au[J]||"default",children:iu[J]||J})},{title:"处理人",dataIndex:"handler_name",width:120,render:J=>J||"--"},{title:"创建时间",dataIndex:"created_at",width:180,render:J=>Ke(J)},{title:"操作",key:"actions",width:160,render:(J,G)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>X(G.id),children:"详情"}),G.status==="pending"&&a.jsx(Y,{size:"small",type:"primary",onClick:()=>_e(G),children:"处理"})]})}];return a.jsxs("div",{className:"admin-reports",children:[a.jsx(be,{className:"admin-reports-filter",children:a.jsxs(b,{form:f,layout:"inline",onFinish:V,children:[a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"目标类型",name:"target_type",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:gj,style:{width:120}})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:_j,style:{width:120}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>f.resetFields(),children:"重置"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:te,dataSource:i,loading:n,scroll:{x:1300},pagination:{current:l,pageSize:d,total:h,showSizeChanger:!0,onChange:(J,G)=>{c(J),u(G),P(J,G)}}})}),a.jsx($i,{title:"举报详情",width:480,open:p,onClose:()=>I(!1),children:a.jsx(Mn,{spinning:S,children:v&&a.jsxs(a.Fragment,{children:[a.jsxs(je,{column:1,bordered:!0,size:"small",children:[a.jsx(je.Item,{label:"ID",children:v.id}),a.jsx(je.Item,{label:"租户 ID",children:v.tenant_id}),a.jsxs(je.Item,{label:"举报人",children:[v.reporter_name,"(ID: ",v.reporter_id,")"]}),a.jsx(je.Item,{label:"目标类型",children:a.jsx(Te,{color:"geekblue",children:ou[v.target_type]||v.target_type})}),a.jsxs(je.Item,{label:"目标名称",children:[v.target_name,"(ID: ",v.target_id,")"]}),a.jsx(je.Item,{label:"举报原因",children:v.reason}),a.jsx(je.Item,{label:"状态",children:a.jsx(Te,{color:au[v.status]||"default",children:iu[v.status]||v.status})}),a.jsx(je.Item,{label:"处理人",children:v.handler_name||"--"}),a.jsx(je.Item,{label:"处理备注",children:v.handle_note||"--"}),a.jsx(je.Item,{label:"创建时间",children:Ke(v.created_at)}),a.jsx(je.Item,{label:"更新时间",children:Ke(v.updated_at)})]}),v.evidence&&v.evidence.length>0&&a.jsxs("div",{style:{marginTop:16},children:[a.jsx(su,{strong:!0,children:"证据图片:"}),a.jsx("div",{style:{marginTop:8},children:a.jsx(Mr.PreviewGroup,{children:v.evidence.map((J,G)=>a.jsx(Mr,{src:J,className:"admin-reports-evidence-img",fallback:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN8/+F9PQAI8wNPvd7POQAAAABJRU5ErkJggg=="},G))})})]})]})})}),a.jsx(Me,{title:"处理举报",open:R,onCancel:()=>j(!1),onOk:B,okText:"确认",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:F,layout:"vertical",children:[a.jsx(b.Item,{label:"处理结果",name:"status",rules:[{required:!0,message:"请选择处理结果"}],children:a.jsxs($e,{placeholder:"选择处理结果",children:[a.jsx($e.Option,{value:"resolved",children:"通过(已处理)"}),a.jsx($e.Option,{value:"rejected",children:"驳回"})]})}),a.jsx(b.Item,{label:"处理备注",name:"handle_note",children:a.jsx(ve.TextArea,{rows:3,placeholder:"可选备注",maxLength:500,showCount:!0})})]})})]})},lu=ks.filter(e=>e.value!=="tenant_manage"),yj=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(1),[o,l]=g.useState(20),[c,d]=g.useState(0),[u,h]=g.useState(),[m,_]=g.useState(!1),[x,f]=g.useState(null),[p]=b.useForm(),[I,S]=g.useState(!1),[w,v]=g.useState(null),[y]=b.useForm(),R=async(J,G=s,se=o)=>{const Q=J??u;if(Q){t(!0);try{const de=await cs.list({tenant_id:Q,page:G,page_size:se});(de.code===0||de.code===200)&&de.data?(n(de.data.items),d(de.data.total),i(de.data.page),l(de.data.size)):E.error(de.message||"获取租户管理员列表失败")}catch(de){E.error(de.message||"获取租户管理员列表失败")}finally{t(!1)}}},j=J=>{h(J),n([]),d(0),i(1),J&&R(J,1,o)},T=()=>{if(!u){E.warning("请先选择租户");return}f(null),p.resetFields(),p.setFieldsValue({permissions:[]}),_(!0)},C=J=>{f(J),p.setFieldsValue({nickname:J.nickname,permissions:Fs(J.permissions)}),_(!0)},F=async()=>{try{const J=await p.validateFields(),G=nc(J.permissions||[]),se=x?await cs.update(x.id,{nickname:J.nickname,permissions:G}):await cs.create({tenant_id:u,username:J.username,password:J.password,nickname:J.nickname,permissions:G});se.code===0||se.code===200?(E.success(x?"租户管理员已更新":"租户管理员已创建"),_(!1),R()):E.error(se.message||"操作失败")}catch(J){if(J!=null&&J.errorFields)return;E.error(J.message||"操作失败")}},P=async J=>{const G=J.status==="active"?"disabled":"active";try{const se=await cs.updateStatus(J.id,G);se.code===0||se.code===200?(E.success("状态已更新"),R()):E.error(se.message||"更新状态失败")}catch(se){E.error(se.message||"更新状态失败")}},V=J=>{v(J),y.resetFields(),S(!0)},X=async()=>{try{const J=await y.validateFields(),G=await cs.resetPassword(w.id,J.password);G.code===0||G.code===200?(E.success("密码已重置"),S(!1)):E.error(G.message||"重置密码失败")}catch(J){if(J!=null&&J.errorFields)return;E.error(J.message||"重置密码失败")}},_e=J=>{Me.confirm({title:"确认删除该租户管理员?",content:"删除后该管理员将无法登录后台管理本租户。",okText:"删除",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const G=await cs.delete(J.id);G.code===0||G.code===200?(E.success("租户管理员已删除"),R()):E.error(G.message||"删除失败")}catch(G){E.error(G.message||"删除失败")}}})},B=J=>{const G=Fs(J);return G.length?a.jsx(Ie,{wrap:!0,children:G.map(se=>{var de;const Q=((de=lu.find(k=>k.value===se))==null?void 0:de.label)||se;return a.jsx(Te,{color:"blue",children:Q},se)})}):"--"},te=[{title:"ID",dataIndex:"id",width:80},{title:"用户名",dataIndex:"username",width:160},{title:"昵称",dataIndex:"nickname",width:160},{title:"权限",dataIndex:"permissions",width:320,render:(J,G)=>B(G.permissions)},{title:"状态",dataIndex:"status",width:100,render:J=>a.jsx(Te,{color:J==="active"?"green":"red",children:J==="active"?"启用":"禁用"})},{title:"最近登录",dataIndex:"last_login_at",width:180,render:J=>Ke(J)},{title:"创建时间",dataIndex:"created_at",width:180,render:J=>Ke(J)},{title:"操作",key:"actions",width:300,render:(J,G)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>C(G),children:"编辑"}),a.jsx(Y,{size:"small",onClick:()=>V(G),children:"重置密码"}),a.jsx(Y,{size:"small",onClick:()=>P(G),children:G.status==="active"?"禁用":"启用"}),a.jsx(Y,{size:"small",danger:!0,onClick:()=>_e(G),children:"删除"})]})}];return a.jsxs("div",{className:"tenant-admins",children:[a.jsx(be,{className:"tenant-admins-filter",children:a.jsxs(b,{layout:"inline",children:[a.jsx(b.Item,{label:"选择租户",children:a.jsx(ot,{style:{width:240},value:u,onChange:j})}),a.jsx(b.Item,{children:a.jsx(Y,{onClick:T,disabled:!u,children:"创建租户管理员"})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:te,dataSource:r,loading:e,scroll:{x:1200},pagination:{current:s,pageSize:o,total:c,showSizeChanger:!0,onChange:(J,G)=>{i(J),l(G),R(void 0,J,G)}}})}),a.jsx(Me,{title:x?"编辑租户管理员":"创建租户管理员",open:m,onCancel:()=>_(!1),onOk:F,okText:"保存",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:p,layout:"vertical",children:[!x&&a.jsxs(a.Fragment,{children:[a.jsx(b.Item,{label:"用户名",name:"username",rules:[{required:!0,message:"请输入用户名"}],children:a.jsx(ve,{placeholder:"小写英文+数字"})}),a.jsx(b.Item,{label:"密码",name:"password",tooltip:"至少 8 位,需包含大小写字母和数字",rules:[{required:!0,message:"请输入密码"},{min:8,message:"密码至少 8 位"},{pattern:/^[\x21-\x7e]+$/,message:"只能包含英文字母、数字和英文特殊字符"},{pattern:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/,message:"必须包含大小写字母和数字"}],children:a.jsx(ve.Password,{placeholder:"请输入密码"})})]}),a.jsx(b.Item,{label:"昵称",name:"nickname",rules:[{required:!0,message:"请输入昵称"}],children:a.jsx(ve,{placeholder:"管理员昵称"})}),a.jsx(b.Item,{label:"权限",name:"permissions",rules:[{required:!0,message:"请选择权限"}],children:a.jsx(gl.Group,{options:lu})})]})}),a.jsx(Me,{title:`重置密码 - ${(w==null?void 0:w.username)||""}`,open:I,onCancel:()=>S(!1),onOk:X,okText:"确认重置",cancelText:"取消",destroyOnClose:!0,children:a.jsx(b,{form:y,layout:"vertical",children:a.jsx(b.Item,{label:"新密码",name:"password",tooltip:"至少 8 位,需包含大小写字母和数字",rules:[{required:!0,message:"请输入新密码"},{min:8,message:"密码至少 8 位"},{pattern:/^[\x21-\x7e]+$/,message:"只能包含英文字母、数字和英文特殊字符"},{pattern:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/,message:"必须包含大小写字母和数字"}],children:a.jsx(ve.Password,{placeholder:"请输入新密码"})})})})]})},wj=()=>{const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),[n,s]=g.useState(!1),[i,o]=g.useState([]),[l,c]=g.useState(1),[d,u]=g.useState(20),[h,m]=g.useState(0),[_,x]=g.useState({}),[f]=b.useForm(),p=async(v=l,y=d,R=_)=>{s(!0);try{const j=await g_.list({...R,page:v,page_size:y});(j.code===0||j.code===200)&&j.data?(o(j.data.items),m(j.data.total),c(j.data.page),u(j.data.size)):E.error(j.message||"获取管理员登录历史失败")}catch(j){E.error(j.message||"获取管理员登录历史失败")}finally{s(!1)}};g.useEffect(()=>{p(1,d,_)},[]);const I=v=>{const y={};return v.admin_id&&(y.admin_id=v.admin_id),v.startTime&&(y.start_time=v.startTime.format("YYYY-MM-DD HH:mm:ss")),v.endTime&&(y.end_time=v.endTime.format("YYYY-MM-DD HH:mm:ss")),y},S=v=>{const y=I(v);x(y),p(1,d,y)},w=[...r?[{title:"ID",dataIndex:"id",width:80}]:[],{title:"用户名",dataIndex:"username",width:160},...r?[{title:"管理员 ID",dataIndex:"admin_id",width:120},{title:"租户名称",dataIndex:"tenant_name",width:160,render:v=>v||a.jsx("span",{style:{color:"#aaa"},children:"平台管理员"})}]:[],{title:"登录时间",dataIndex:"login_time",width:180,render:v=>Ke(v)},{title:"User-Agent",dataIndex:"device_type",ellipsis:!0,render:v=>a.jsx(Te,{color:"blue",style:{maxWidth:300,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:v})},{title:"IP 地址",dataIndex:"ip",width:140}];return a.jsxs("div",{className:"admin-login-history",children:[a.jsx(be,{className:"admin-login-history-filter",children:a.jsxs(b,{form:f,layout:"inline",onFinish:S,children:[r&&a.jsx(b.Item,{label:"管理员 ID",name:"admin_id",children:a.jsx(at,{placeholder:"可选",min:1,style:{width:150}})}),a.jsx(b.Item,{label:"开始时间",name:"startTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{label:"结束时间",name:"endTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{children:a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:w,dataSource:i,loading:n,scroll:{x:r?1300:900},pagination:{current:l,pageSize:d,total:h,showSizeChanger:!0,onChange:(v,y)=>{c(v),u(y),p(v,y)}}})})]})},Sj=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,[r,n]=g.useState(!1),[s,i]=g.useState([]),[o,l]=g.useState(1),[c,d]=g.useState(20),[u,h]=g.useState(0),[m,_]=g.useState({}),[x]=b.useForm(),f=async(v=o,y=c,R=m)=>{n(!0);try{const j=await __.list({...R,page:v,page_size:y});(j.code===0||j.code===200)&&j.data?(i(j.data.items),h(j.data.total),l(j.data.page),d(j.data.size)):E.error(j.message||"获取用户登录历史失败")}catch(j){E.error(j.message||"获取用户登录历史失败")}finally{n(!1)}};g.useEffect(()=>{f(1,c,m)},[]);const p=v=>{const y={};return v.tenant_id&&(y.tenant_id=Number(v.tenant_id)),v.user_id&&(y.user_id=Number(v.user_id)),v.user_no&&(y.user_no=Number(v.user_no)),v.username&&(y.username=v.username.trim()),v.startTime&&(y.start_time=v.startTime.format("YYYY-MM-DD HH:mm:ss")),v.endTime&&(y.end_time=v.endTime.format("YYYY-MM-DD HH:mm:ss")),v.ip&&(y.ip=v.ip.trim()),y},I=v=>{const y=p(v);_(y),f(1,c,y)},S=v=>{if(v.user_status==="deleted")return;const y=v.user_status==="disabled";Me.confirm({title:y?"重新强制下线":"封禁用户",content:y?`用户「${v.username}」已封禁,确认再次撤销全部登录会话并强制下线?`:`确认封禁用户「${v.username}」?封禁后会立即撤销全部登录会话并强制下线。`,okText:y?"确认下线":"确认封禁",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{try{const R=await Lt.updateStatus(v.user_id,"disabled");R.code===0||R.code===200?E.success("用户已封禁并强制下线"):E.error(R.message||"封禁失败")}catch(R){E.error(R.message||"封禁失败")}}})},w=[...t?[{title:"ID",dataIndex:"id",width:80}]:[],...t?[{title:"所属私域",dataIndex:"tenant_name",width:160,render:(v,y)=>a.jsxs("span",{children:[y.tenant_name," (ID: ",y.tenant_id,")"]})},{title:"用户 ID",dataIndex:"user_id",width:120},{title:"用户名",dataIndex:"username",width:160}]:[{title:"用户编号",dataIndex:"user_no",width:120},{title:"用户名",dataIndex:"username",width:160}],{title:"当前状态",dataIndex:"user_status",width:100,render:v=>{const y={active:{color:"green",label:"正常"},disabled:{color:"red",label:"已封禁"},deleted:{color:"default",label:"已注销"}},R=y[v]||y.disabled;return a.jsx(Te,{color:R.color,children:R.label})}},{title:"登录时间",dataIndex:"login_time",width:180,render:v=>Ke(v)},{title:"设备类型",dataIndex:"device_type",width:100,render:v=>{const y={android:"green",ios:"blue",web:"orange"};return a.jsx(Te,{color:y[v]||"default",children:v})}},{title:"设备型号",dataIndex:"device_name",width:160,render:(v,y)=>y.device_type==="web"?y.device_model||"-":y.device_name||y.device_model||"-"},{title:"系统版本",dataIndex:"os_version",width:120,render:v=>v?a.jsx(Te,{color:"purple",children:v}):"-"},{title:"App 版本",dataIndex:"version",width:100,render:v=>v?a.jsx(Te,{color:"geekblue",children:v}):"-"},{title:"IP 地址",dataIndex:"ip",width:140},{title:"归属地",dataIndex:"location",width:120,render:v=>a.jsx(Te,{color:"green",children:v})},{title:"操作",key:"actions",fixed:"right",width:100,render:(v,y)=>a.jsx(Y,{size:"small",danger:!0,disabled:y.user_status==="deleted",onClick:()=>S(y),children:y.user_status==="disabled"?"重新下线":y.user_status==="deleted"?"已注销":"快速封禁"})}];return a.jsxs("div",{className:"user-login-history",children:[a.jsx(be,{className:"user-login-history-filter",children:a.jsxs(b,{form:x,layout:"inline",onFinish:I,children:[t&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),t?a.jsx(b.Item,{label:"用户 ID",name:"user_id",children:a.jsx(ve,{placeholder:"可选",style:{width:150}})}):a.jsx(b.Item,{label:"用户编号",name:"user_no",children:a.jsx(ve,{placeholder:"可选",style:{width:150}})}),a.jsx(b.Item,{label:"用户名",name:"username",children:a.jsx(ve,{placeholder:"可选",style:{width:150}})}),a.jsx(b.Item,{label:"开始时间",name:"startTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{label:"结束时间",name:"endTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{label:"IP",name:"ip",children:a.jsx(ve,{placeholder:"登录IP",allowClear:!0,style:{width:150}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:()=>x.resetFields(),children:"重置"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:w,dataSource:s,loading:r,scroll:{x:t?1920:1720},pagination:{current:o,pageSize:c,total:u,showSizeChanger:!0,onChange:(v,y)=>{l(v),d(y),f(v,y)}}})})]})},Tj=[{label:"当前连续天数",value:"current_streak"},{label:"历史最长天数",value:"max_streak"},{label:"累计登录天数",value:"total_login_days"}],bj=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,[r,n]=g.useState(!1),[s,i]=g.useState([]),[o,l]=g.useState(1),[c,d]=g.useState(20),[u,h]=g.useState(0),[m,_]=g.useState("current_streak"),[x,f]=g.useState({}),[p]=b.useForm(),I=async(j=o,T=c,C=x,F=m)=>{n(!0);try{const P=await y_.list({...C,order_by:F,page:j,page_size:T});(P.code===0||P.code===200)&&P.data?(i(P.data.items),h(P.data.total),l(P.data.page),d(P.data.size)):E.error(P.message||"获取登录排名失败")}catch(P){E.error(P.message||"获取登录排名失败")}finally{n(!1)}};g.useEffect(()=>{I(1,c,x,m)},[]);const S=j=>{const T={};return j.tenant_id&&(T.tenant_id=Number(j.tenant_id)),j.invite_code&&(T.invite_code=j.invite_code.trim()),j.keyword&&(T.keyword=j.keyword.trim()),T},w=j=>{const T=S(j);f(T),I(1,c,T,m)},v=j=>{_(j),I(1,c,x,j)},y=j=>j===1?a.jsx(Te,{color:"gold",children:"🥇 1"}):j===2?a.jsx(Te,{color:"silver",children:"🥈 2"}):j===3?a.jsx(Te,{color:"volcano",children:"🥉 3"}):a.jsx("span",{children:j}),R=[{title:"排名",dataIndex:"rank",width:90,render:j=>y(j)},...t?[{title:"所属私域",dataIndex:"tenant_name",width:180,render:(j,T)=>a.jsxs("span",{children:[T.tenant_name," (ID: ",T.tenant_id,")"]})}]:[{title:"用户编号",dataIndex:"user_no",width:110}],{title:"用户",dataIndex:"nickname",width:200,render:(j,T)=>a.jsxs("span",{children:[T.nickname||T.username,a.jsxs("span",{className:"login-streak-username",children:["(",T.username,")"]})]})},{title:"邀请码",dataIndex:"invite_code",width:120,render:j=>j?a.jsx(Te,{color:"blue",children:j}):"-"},{title:a.jsx(Xt,{title:"截至昨天的连续登录天数,断签立即归零",children:a.jsx("span",{children:"当前连续"})}),dataIndex:"current_streak",width:110,render:j=>a.jsxs(Te,{color:j>0?"green":"default",children:[j," 天"]})},{title:"历史最长",dataIndex:"max_streak",width:110,render:j=>a.jsxs(Te,{color:"geekblue",children:[j," 天"]})},{title:"累计登录",dataIndex:"total_login_days",width:110,render:j=>a.jsxs("span",{children:[j," 天"]})},{title:"最近登录日",dataIndex:"last_login_day",width:130,render:j=>j||"-"},{title:"状态",dataIndex:"user_status",width:90,render:j=>{const T={active:{color:"green",label:"正常"},disabled:{color:"red",label:"已封禁"},deleted:{color:"default",label:"已注销"}},C=T[j]||T.disabled;return a.jsx(Te,{color:C.color,children:C.label})}}];return a.jsxs("div",{className:"login-streak-ranking",children:[a.jsx(be,{className:"login-streak-ranking-filter",children:a.jsxs(b,{form:p,layout:"inline",onFinish:w,children:[t&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"邀请码",name:"invite_code",children:a.jsx(ve,{placeholder:"按团队邀请码",allowClear:!0,style:{width:150}})}),a.jsx(b.Item,{label:"用户",name:"keyword",children:a.jsx(ve,{placeholder:"用户名/昵称",allowClear:!0,style:{width:160}})}),a.jsx(b.Item,{label:"排序",children:a.jsx($e,{value:m,onChange:v,options:Tj,style:{width:150}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:()=>p.resetFields(),children:"重置"})]})})]})}),a.jsx(be,{title:a.jsxs("span",{children:[a.jsx(bu,{style:{color:"#faad14",marginRight:8}}),"连续登录排行榜"]}),extra:a.jsx("span",{className:"login-streak-ranking-hint",children:"每日 00:05 更新(统计截至昨天)"}),children:a.jsx(Ye,{rowKey:"user_id",columns:R,dataSource:s,loading:r,scroll:{x:t?1300:1200},pagination:{current:o,pageSize:c,total:u,showSizeChanger:!0,showTotal:j=>`共 ${j} 人`,onChange:(j,T)=>{l(j),d(T),I(j,T)}}})})]})},Ej=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,[r,n]=g.useState(!1),[s,i]=g.useState([]),[o,l]=g.useState(1),[c,d]=g.useState(20),[u,h]=g.useState(0),[m,_]=g.useState({}),[x]=b.useForm(),f=async(y=o,R=c,j=m)=>{n(!0);try{const T=await w_.list({...j,page:y,page_size:R});(T.code===0||T.code===200)&&T.data?(i(T.data.items),h(T.data.total),l(T.data.page),d(T.data.size)):E.error(T.message||"获取单聊消息排名失败")}catch(T){E.error(T.message||"获取单聊消息排名失败")}finally{n(!1)}};g.useEffect(()=>{f(1,c,m)},[]);const p=y=>{const R={};return y.tenant_id&&(R.tenant_id=Number(y.tenant_id)),y.invite_code&&(R.invite_code=y.invite_code.trim()),y.keyword&&(R.keyword=y.keyword.trim()),R},I=y=>{const R=p(y);_(R),f(1,c,R)},S=y=>y===1?a.jsx(Te,{color:"gold",children:"🥇 1"}):y===2?a.jsx(Te,{color:"silver",children:"🥈 2"}):y===3?a.jsx(Te,{color:"volcano",children:"🥉 3"}):a.jsx("span",{children:y}),w=(y,R,j)=>a.jsxs("span",{children:[y||R,a.jsxs("span",{className:"private-msg-username",children:["(",R,")"]}),j?a.jsx(Te,{color:"blue",style:{marginLeft:6},children:j}):null]}),v=[{title:"排名",dataIndex:"rank",width:80,render:y=>S(y)},...t?[{title:"所属私域",dataIndex:"tenant_name",width:160,render:(y,R)=>a.jsxs("span",{children:[R.tenant_name," (ID: ",R.tenant_id,")"]})}]:[],{title:"会话",dataIndex:"conversation_id",width:110,render:y=>a.jsxs("span",{children:["#",y]})},{title:"用户 A",dataIndex:"user_a_id",width:220,render:(y,R)=>w(R.user_a_nickname,R.user_a_username,R.invite_code_a)},{title:"A 发送",dataIndex:"count_a",width:100,align:"center",render:y=>a.jsxs(Te,{color:"geekblue",children:[y," 条"]})},{title:"用户 B",dataIndex:"user_b_id",width:220,render:(y,R)=>w(R.user_b_nickname,R.user_b_username,R.invite_code_b)},{title:"B 发送",dataIndex:"count_b",width:100,align:"center",render:y=>a.jsxs(Te,{color:"purple",children:[y," 条"]})},{title:"会话总数",dataIndex:"total_count",width:120,align:"center",render:y=>a.jsxs(Te,{color:"green",children:[y," 条"]})},{title:"最近发言",dataIndex:"last_msg_at",width:180,render:y=>y||"-"}];return a.jsxs("div",{className:"private-msg-ranking",children:[a.jsx(be,{className:"private-msg-ranking-filter",children:a.jsxs(b,{form:x,layout:"inline",onFinish:I,children:[t&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"邀请码",name:"invite_code",children:a.jsx(ve,{placeholder:"按团队邀请码",allowClear:!0,style:{width:150}})}),a.jsx(b.Item,{label:"用户",name:"keyword",children:a.jsx(ve,{placeholder:"用户名/昵称",allowClear:!0,style:{width:160}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:()=>x.resetFields(),children:"重置"})]})})]})}),a.jsx(be,{title:a.jsxs("span",{children:[a.jsx(ci,{style:{color:"#52c41a",marginRight:8}}),"单聊消息数量排行榜"]}),extra:a.jsx("span",{className:"private-msg-ranking-hint",children:"每日 00:05 更新(含撤回/删除已剔除)"}),children:a.jsx(Ye,{rowKey:"conversation_id",columns:v,dataSource:s,loading:r,scroll:{x:t?1400:1250},pagination:{current:o,pageSize:c,total:u,showSizeChanger:!0,showTotal:y=>`共 ${y} 个会话`,onChange:(y,R)=>{l(y),d(R),f(y,R)}}})})]})},jj=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState(null),[s,i]=g.useState(!1),[o]=b.useForm(),l=async c=>{const d=(c.keyword||"").trim();if(!c.tenant_id){E.warning("请选择租户");return}if(!d){E.warning("请输入用户名或手机号");return}t(!0),i(!0);try{const u=await S_.lookup({tenant_id:Number(c.tenant_id),keyword:d});(u.code===0||u.code===200)&&u.data?n(u.data):(n(null),E.error(u.message||"查询失败"))}catch(u){n(null),E.error(u.message||"查询失败")}finally{t(!1)}};return a.jsxs("div",{className:"user-password-query",children:[a.jsx(be,{className:"user-password-query-filter",children:a.jsxs(b,{form:o,layout:"inline",onFinish:l,children:[a.jsx(b.Item,{label:"租户",name:"tenant_id",rules:[{required:!0,message:"请选择租户"}],children:a.jsx(ot,{style:{width:220}})}),a.jsx(b.Item,{label:"用户",name:"keyword",rules:[{required:!0,message:"请输入用户名或手机号"}],children:a.jsx(ve,{placeholder:"用户名或手机号",allowClear:!0,style:{width:220}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),loading:e,children:"查询"}),a.jsx(Y,{onClick:()=>{o.resetFields(),n(null),i(!1)},children:"重置"})]})})]})}),a.jsxs(be,{title:a.jsxs("span",{children:[a.jsx(xi,{style:{color:"#1677ff",marginRight:8}}),"查询结果"]}),children:[!s&&a.jsx("div",{className:"user-password-query-empty",children:"请选择租户并输入用户名或手机号后查询"}),s&&!r&&!e&&a.jsx("div",{className:"user-password-query-empty",children:"未查到对应用户"}),r&&a.jsxs(je,{bordered:!0,column:1,size:"middle",children:[a.jsxs(je.Item,{label:"所属私域",children:[r.tenant_name,"(ID: ",r.tenant_id,")"]}),a.jsx(je.Item,{label:"用户编号",children:r.user_no||"-"}),a.jsx(je.Item,{label:"用户名",children:r.username}),a.jsx(je.Item,{label:"昵称",children:r.nickname||"-"}),a.jsx(je.Item,{label:"手机号",children:r.phone||"-"}),a.jsx(je.Item,{label:"密码",children:r.password?a.jsx(Te,{color:"blue",className:"user-password-value",children:r.password}):a.jsx("span",{className:"user-password-missing",children:"未记录该密码"})})]})]})]})},Ij=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(1),[o,l]=g.useState(20),[c,d]=g.useState(0),[u,h]=g.useState({}),[m,_]=g.useState("updated_at"),[x]=b.useForm(),f=async(w=s,v=o,y=u,R=m)=>{t(!0);try{const j=await ps.listAlerts({...y,sort_by:R,page:w,page_size:v});(j.code===0||j.code===200)&&j.data?(n(j.data.items),d(j.data.total),i(j.data.page),l(j.data.size)):E.error(j.message||"获取预警记录失败")}catch(j){E.error(j.message||"获取预警记录失败")}finally{t(!1)}};g.useEffect(()=>{f(1,o,u,m)},[]);const p=w=>{const v={};w.tenant_id&&(v.tenant_id=Number(w.tenant_id)),w.keyword&&(v.keyword=w.keyword.trim()),w.conversation_id&&(v.conversation_id=Number(w.conversation_id)),h(v),f(1,o,v,m)},I=(w,v)=>a.jsxs("span",{children:[w||v,a.jsxs("span",{className:"keyword-alert-username",children:["(",v,")"]})]}),S=[{title:"会话",dataIndex:"conversation_id",width:100,render:w=>`#${w}`},{title:"所属私域",dataIndex:"tenant_name",width:160,render:(w,v)=>`${v.tenant_name}(ID:${v.tenant_id})`},{title:"用户 A",dataIndex:"user_a_id",width:200,render:(w,v)=>I(v.user_a_nickname,v.user_a_username)},{title:"用户 B",dataIndex:"user_b_id",width:200,render:(w,v)=>I(v.user_b_nickname,v.user_b_username)},{title:"关键词",dataIndex:"keyword",width:140,render:w=>a.jsx(Te,{color:"red",children:w})},{title:"出现次数",dataIndex:"hit_count",width:110,align:"center",render:w=>a.jsxs(Te,{color:"volcano",children:[w," 次"]})},{title:"最后更新时间",dataIndex:"updated_at",width:180}];return a.jsxs(a.Fragment,{children:[a.jsx(be,{className:"keyword-alert-filter",children:a.jsxs(b,{form:x,layout:"inline",onFinish:p,children:[a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"按关键词",allowClear:!0,style:{width:150}})}),a.jsx(b.Item,{label:"会话ID",name:"conversation_id",children:a.jsx(at,{placeholder:"会话ID",style:{width:140}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:()=>x.resetFields(),children:"重置"})]})})]})}),a.jsx(be,{title:a.jsxs("span",{children:[a.jsx(Su,{style:{color:"#cf1322",marginRight:8}}),"单聊关键词预警"]}),extra:a.jsxs(Ie,{children:[a.jsx("span",{className:"keyword-alert-hint",children:"排序"}),a.jsx(Y,{size:"small",type:m==="updated_at"?"primary":"default",onClick:()=>{_("updated_at"),f(1,o,u,"updated_at")},children:"最近更新"}),a.jsx(Y,{size:"small",type:m==="hit_count"?"primary":"default",onClick:()=>{_("hit_count"),f(1,o,u,"hit_count")},children:"出现次数"})]}),children:a.jsx(Ye,{rowKey:"id",columns:S,dataSource:r,loading:e,scroll:{x:1200},pagination:{current:s,pageSize:o,total:c,showSizeChanger:!0,showTotal:w=>`共 ${w} 条`,onChange:(w,v)=>{i(w),l(v),f(w,v,u,m)}}})})]})},Cj=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(1),[o,l]=g.useState(20),[c,d]=g.useState(0),[u,h]=g.useState(""),[m,_]=g.useState(!1),[x,f]=g.useState(null),[p]=b.useForm(),I=async(T=s,C=o,F=u)=>{t(!0);try{const P=await ps.listKeywords({keyword:F||void 0,page:T,page_size:C});(P.code===0||P.code===200)&&P.data?(n(P.data.items),d(P.data.total),i(P.data.page),l(P.data.size)):E.error(P.message||"获取词库失败")}catch(P){E.error(P.message||"获取词库失败")}finally{t(!1)}};g.useEffect(()=>{I(1,o,"")},[]);const S=()=>{f(null),p.resetFields(),p.setFieldsValue({enabled:!0}),_(!0)},w=T=>{f(T),p.setFieldsValue({word:T.word,enabled:T.enabled}),_(!0)},v=async()=>{const T=await p.validateFields();try{if(x){const C=await ps.updateKeyword(x.id,{word:T.word.trim(),enabled:T.enabled});if(C.code===0||C.code===200)E.success("已更新");else{E.error(C.message||"更新失败");return}}else{const C=await ps.createKeyword({word:T.word.trim(),enabled:T.enabled});if(C.code===0||C.code===200)E.success("已新增");else{E.error(C.message||"新增失败");return}}_(!1),I(x?s:1,o,u)}catch(C){if(C!=null&&C.errorFields)return;E.error(C.message||"操作失败")}},y=async(T,C)=>{const F=await ps.updateKeyword(T.id,{enabled:C});F.code===0||F.code===200?(E.success(C?"已启用":"已禁用"),I(s,o,u)):E.error(F.message||"操作失败")},R=async T=>{const C=await ps.deleteKeyword(T.id);C.code===0||C.code===200?(E.success("已删除"),I(s,o,u)):E.error(C.message||"删除失败")},j=[{title:"关键词",dataIndex:"word",width:200},{title:"状态",dataIndex:"enabled",width:120,render:(T,C)=>a.jsx(rt,{checked:T,onChange:F=>y(C,F)})},{title:"创建时间",dataIndex:"created_at",width:180},{title:"操作",key:"action",width:160,render:(T,C)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>w(C),children:"编辑"}),a.jsx(js,{title:"确认删除该关键词?",onConfirm:()=>R(C),children:a.jsx(Y,{size:"small",danger:!0,children:"删除"})})]})}];return a.jsxs(a.Fragment,{children:[a.jsx(be,{className:"keyword-alert-filter",children:a.jsxs(Ie,{children:[a.jsx(ve,{placeholder:"搜索关键词",allowClear:!0,style:{width:200},value:u,onChange:T=>h(T.target.value),onPressEnter:()=>I(1,o,u)}),a.jsx(Y,{type:"primary",icon:a.jsx(Ar,{}),onClick:()=>I(1,o,u),children:"查询"}),a.jsx(Y,{icon:a.jsx(ua,{}),onClick:S,children:"新增关键词"})]})}),a.jsx(be,{title:"预警词库(全平台)",children:a.jsx(Ye,{rowKey:"id",columns:j,dataSource:r,loading:e,pagination:{current:s,pageSize:o,total:c,showSizeChanger:!0,showTotal:T=>`共 ${T} 个`,onChange:(T,C)=>{i(T),l(C),I(T,C,u)}}})}),a.jsx(Me,{title:x?"编辑关键词":"新增关键词",open:m,onOk:v,onCancel:()=>_(!1),destroyOnClose:!0,children:a.jsxs(b,{form:p,layout:"vertical",children:[a.jsx(b.Item,{label:"关键词",name:"word",rules:[{required:!0,message:"请输入关键词"}],children:a.jsx(ve,{placeholder:"请输入关键词",maxLength:200})}),a.jsx(b.Item,{label:"启用",name:"enabled",valuePropName:"checked",children:a.jsx(rt,{})})]})})]})},Aj=()=>a.jsx("div",{className:"keyword-alert",children:a.jsx(zi,{defaultActiveKey:"alerts",items:[{key:"alerts",label:"预警记录",children:a.jsx(Ij,{})},{key:"keywords",label:"预警词库",children:a.jsx(Cj,{})}]})}),kj=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState(!1),[s]=b.useForm(),i=async()=>{t(!0);try{const l=await nd.get();(l.code===0||l.code===200)&&l.data?s.setFieldsValue({api_url:l.data.api_url||"",bot_token:l.data.bot_token||"",chat_id:l.data.chat_id||"",alert_every:l.data.alert_every||5,total_login_days:l.data.total_login_days??0}):E.error(l.message||"获取配置失败")}catch(l){E.error(l.message||"获取配置失败")}finally{t(!1)}};g.useEffect(()=>{i()},[]);const o=async l=>{n(!0);try{const c=await nd.update({api_url:(l.api_url||"").trim(),bot_token:(l.bot_token||"").trim(),chat_id:(l.chat_id||"").trim(),alert_every:Number(l.alert_every)||5,total_login_days:Number(l.total_login_days)||0});c.code===0||c.code===200?(E.success("已保存"),c.data&&s.setFieldsValue({api_url:c.data.api_url,bot_token:c.data.bot_token,chat_id:c.data.chat_id,alert_every:c.data.alert_every,total_login_days:c.data.total_login_days})):E.error(c.message||"保存失败")}catch(c){E.error(c.message||"保存失败")}finally{n(!1)}};return a.jsx("div",{className:"alert-bot-config",children:a.jsx(be,{loading:e,title:a.jsxs("span",{children:[a.jsx(Tu,{style:{marginRight:8}}),"预警机器人配置"]}),extra:a.jsx("span",{className:"alert-bot-config-hint",children:"全平台一套,保存后立即生效"}),children:a.jsxs(b,{form:s,layout:"vertical",style:{maxWidth:640},onFinish:o,initialValues:{api_url:"",alert_every:5,total_login_days:0},children:[a.jsx(b.Item,{label:"通知 API 地址",name:"api_url",rules:[{required:!0,message:"请输入通知 API 地址"}],extra:"完整发送地址,如 http://127.0.0.1:8080/send(Form: tgtoken/chatid/msg)",children:a.jsx(ve,{placeholder:"http://127.0.0.1:8080/send",allowClear:!0})}),a.jsx(b.Item,{label:"TG Token",name:"bot_token",rules:[{required:!0,message:"请输入 TG Token"}],children:a.jsx(ve.Password,{placeholder:"BotFather 发放的 bot token",allowClear:!0})}),a.jsx(b.Item,{label:"Chat ID",name:"chat_id",rules:[{required:!0,message:"请输入 Chat ID"}],children:a.jsx(ve,{placeholder:"目标群 chat_id,如 -5227827060",allowClear:!0})}),a.jsx(b.Item,{label:"通知频率 alert_every",name:"alert_every",rules:[{required:!0,message:"请输入通知频率"}],extra:"关键词命中次数达到该值的倍数时推送,默认 5",children:a.jsx(at,{min:1,max:1e4,style:{width:"100%"}})}),a.jsx(b.Item,{label:"累计登录天数 total_login_days",name:"total_login_days",rules:[{required:!0,message:"请输入累计登录天数阈值"}],extra:"运营配置项;封禁用户时若其累计登录天数≥该值则机器人通知;0=关闭",children:a.jsx(at,{min:0,max:1e5,style:{width:"100%"}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Js,{}),loading:r,children:"保存"}),a.jsx(Y,{onClick:i,disabled:e||r,children:"重新加载"})]})})]})})})},Fj=e=>{const t=Math.floor(e/86400),r=Math.floor(e%86400/3600),n=Math.floor(e%3600/60);return t>0?`${t}天 ${r}小时 ${n}分钟`:r>0?`${r}小时 ${n}分钟`:`${n}分钟`},Oj=()=>{const[e,t]=g.useState(!1),[r,n]=g.useState([]),[s,i]=g.useState(""),o=g.useRef(null),l=g.useCallback(async()=>{t(!0);try{const h=await T_.getAllNodesStatus();(h.code===0||h.code===200)&&h.data?(n(h.data),!s&&h.data.length>0&&i(h.data[0].node_id)):E.error(h.message||"获取系统状态失败")}catch(h){E.error(h.message||"获取系统状态失败")}finally{t(!1)}},[s]);g.useEffect(()=>(l(),o.current=setInterval(l,3e4),()=>{o.current&&clearInterval(o.current)}),[]);const c=r.find(h=>h.node_id===s),d=h=>{var _,x,f,p;if(h.error)return a.jsx(Zs,{type:"error",message:"节点不可达",description:h.error,showIcon:!0});const m=h.db.max_open>0?Math.round(h.db.in_use/h.db.max_open*100):0;return a.jsxs(a.Fragment,{children:[a.jsxs(Es,{gutter:[16,16],style:{marginBottom:16},children:[a.jsx(Ct,{xs:24,sm:12,lg:6,children:a.jsx(be,{children:a.jsx(tn,{title:"Goroutines",value:h.go.goroutines})})}),a.jsx(Ct,{xs:24,sm:12,lg:6,children:a.jsx(be,{children:a.jsx(tn,{title:"堆内存",value:h.go.heap_alloc_mb,suffix:"MB",precision:2})})}),a.jsx(Ct,{xs:24,sm:12,lg:6,children:a.jsx(be,{children:a.jsx(tn,{title:"在线用户",value:h.ws.online_users})})}),a.jsx(Ct,{xs:24,sm:12,lg:6,children:a.jsx(be,{children:a.jsx(tn,{title:"DB 连接使用率",value:m,suffix:"%",valueStyle:{color:m>80?"#cf1322":void 0}})})})]}),a.jsxs(Es,{gutter:[16,16],children:[a.jsx(Ct,{xs:24,lg:12,children:a.jsx(be,{title:"Go 运行时",size:"small",children:a.jsxs(je,{column:2,size:"small",children:[a.jsx(je.Item,{label:"版本",children:h.go.version}),a.jsx(je.Item,{label:"运行时长",children:Fj(h.go.uptime_sec)}),a.jsx(je.Item,{label:"GC 次数",children:h.go.num_gc}),a.jsxs(je.Item,{label:"系统内存",children:[h.go.sys_mem_mb," MB"]}),a.jsx(je.Item,{label:"Goroutines",children:h.go.goroutines}),a.jsxs(je.Item,{label:"堆内存",children:[h.go.heap_alloc_mb," MB"]})]})})}),a.jsx(Ct,{xs:24,lg:12,children:a.jsxs(be,{title:"数据库连接池",size:"small",children:[a.jsx("div",{style:{marginBottom:12},children:a.jsx(sm,{percent:m,format:()=>`${h.db.in_use} / ${h.db.max_open}`,status:m>80?"exception":"active"})}),a.jsxs(je,{column:2,size:"small",children:[a.jsx(je.Item,{label:"最大连接",children:h.db.max_open}),a.jsx(je.Item,{label:"当前打开",children:h.db.open}),a.jsx(je.Item,{label:"使用中",children:h.db.in_use}),a.jsx(je.Item,{label:"空闲",children:h.db.idle}),a.jsx(je.Item,{label:"等待次数",children:h.db.wait_count})]})]})}),a.jsx(Ct,{xs:24,lg:12,children:a.jsx(be,{title:"Redis",size:"small",children:a.jsxs(je,{column:2,size:"small",children:[a.jsx(je.Item,{label:"状态",children:h.redis.connected?a.jsx(Te,{color:"green",children:"已连接"}):a.jsx(Te,{color:"red",children:"断开"})}),a.jsx(je.Item,{label:"内存用量",children:h.redis.used_memory||"N/A"}),a.jsx(je.Item,{label:"客户端连接",children:h.redis.connected_clients||"N/A"}),a.jsx(je.Item,{label:"命中率",children:h.redis.hit_rate||"N/A"}),a.jsxs(je.Item,{label:"运行天数",children:[h.redis.uptime_days||"N/A"," 天"]})]})})}),a.jsx(Ct,{xs:24,lg:12,children:a.jsx(be,{title:"WebSocket",size:"small",children:a.jsxs(je,{column:2,size:"small",children:[a.jsx(je.Item,{label:"在线用户",children:h.ws.online_users}),a.jsx(je.Item,{label:"Hub 连接数",children:h.ws.connections})]})})}),a.jsx(Ct,{xs:24,lg:12,children:a.jsx(be,{title:"OSS 存储",size:"small",children:a.jsxs(je,{column:2,size:"small",children:[a.jsx(je.Item,{label:"桶名称",children:((_=h.oss)==null?void 0:_.bucket)||"N/A"}),a.jsx(je.Item,{label:"已用空间",children:((x=h.oss)==null?void 0:x.storage_human)||"N/A"}),a.jsx(je.Item,{label:"文件数量",children:((p=(f=h.oss)==null?void 0:f.object_count)==null?void 0:p.toLocaleString())??"N/A"})]})})})]})]})},u=r.map(h=>({key:h.node_id,label:a.jsxs("span",{children:[a.jsx("span",{className:`node-badge ${h.error?"node-badge-error":"node-badge-ok"}`}),h.node_name||h.node_id]})}));return a.jsxs("div",{className:"system-status",children:[a.jsxs("div",{className:"system-status-header",children:[a.jsx("h3",{children:"系统状态"}),a.jsx(Y,{icon:a.jsx(nm,{}),loading:e,onClick:l,children:"刷新"})]}),r.length>1?a.jsx(zi,{activeKey:s,onChange:i,items:u}):r.length===1&&a.jsxs("div",{style:{marginBottom:8},children:[a.jsx(Te,{color:"blue",children:r[0].node_name||r[0].node_id}),r[0].server_time&&a.jsxs("span",{style:{color:"rgba(0,0,0,0.45)",fontSize:13},children:["服务器时间:",r[0].server_time]})]}),c&&d(c),c&&!c.error&&c.server_time&&r.length>1&&a.jsxs("div",{style:{textAlign:"right",color:"rgba(0,0,0,0.45)",fontSize:12},children:["服务器时间:",c.server_time]})]})},{Text:cu}=Cr,Rj=()=>{const{admin:e,permissions:t}=pt(),r=!!e&&!e.tenant_id&&!!(t!=null&&t.tenant_manage),[n,s]=g.useState(!1),[i,o]=g.useState(!1),[l,c]=g.useState(null),[d,u]=g.useState(),h=g.useRef(0),[m]=b.useForm(),_=async f=>{const p=++h.current;s(!0);try{const I=f?{tenant_id:f}:void 0,S=await sd.get(I);if(p!==h.current)return;(S.code===0||S.code===200)&&S.data?(c(S.data),m.setFieldsValue({max_text_length:S.data.max_text_length,max_image_size_mb:S.data.max_image_size_mb,max_file_size_mb:S.data.max_file_size_mb,max_recall_minutes:S.data.max_recall_minutes??2,enable_security_perm:S.data.enable_security_perm??!1,enable_sms_code:S.data.enable_sms_code??!0})):E.error(S.message||"获取设置失败")}catch(I){if(p!==h.current)return;E.error(I.message||"获取设置失败")}finally{p===h.current&&s(!1)}};g.useEffect(()=>{e!=null&&e.tenant_id&&_()},[e==null?void 0:e.tenant_id]);const x=async()=>{if(r&&(!d||(l==null?void 0:l.tenant_id)!==d)){E.error("请先选择租户并等待配置加载完成");return}try{const f=await m.validateFields();o(!0);const p={max_text_length:f.max_text_length,max_image_size_mb:f.max_image_size_mb,max_file_size_mb:f.max_file_size_mb,max_recall_minutes:f.max_recall_minutes,enable_sms_code:f.enable_sms_code};r&&(p.enable_security_perm=f.enable_security_perm),r&&(p.tenant_id=d);const I=await sd.update(p);I.code===0||I.code===200?E.success("设置已保存"):E.error(I.message||"保存失败")}catch(f){if(f!=null&&f.errorFields)return;E.error(f.message||"保存失败")}finally{o(!1)}};return a.jsx("div",{className:"admin-tenant-settings",children:a.jsx(be,{title:l?`私域设置 - ${l.tenant_name}`:"私域设置",children:a.jsx(Mn,{spinning:n,children:a.jsxs(b,{form:m,layout:"vertical",style:{maxWidth:480},children:[r&&a.jsx(b.Item,{label:"租户",children:a.jsx(ot,{value:d,placeholder:"请选择租户",allowClear:!0,disabled:i,style:{width:"100%"},onChange:f=>{h.current+=1,u(typeof f=="number"?f:void 0),c(null),m.resetFields(),typeof f=="number"?_(f):s(!1)}})}),a.jsx(b.Item,{label:"消息字符上限",name:"max_text_length",rules:[{required:!0,message:"请输入消息字符上限"}],extra:"范围 1~5000,仅允许调低",children:a.jsx(at,{min:1,max:5e3,style:{width:"100%"}})}),a.jsx(b.Item,{label:"图片大小上限(MB)",name:"max_image_size_mb",rules:[{required:!0,message:"请输入图片大小上限"}],extra:"范围 1~1000 MB",children:a.jsx(at,{min:1,max:1e3,style:{width:"100%"}})}),a.jsx(b.Item,{label:"文件大小上限(MB)",name:"max_file_size_mb",rules:[{required:!0,message:"请输入文件大小上限"}],extra:"范围 1~1000 MB,默认 50 MB",children:a.jsx(at,{min:1,max:1e3,style:{width:"100%"}})}),a.jsx(b.Item,{label:"最大撤回消息时间(分钟)",name:"max_recall_minutes",rules:[{required:!0,message:"请输入最大撤回消息时间"}],extra:"群主/管理员撤回自己或他人消息的最长时限,范围 1~1440 分钟;普通用户撤回自己的消息固定为 2 分钟",children:a.jsx(at,{min:1,max:1440,precision:0,style:{width:"100%"}})}),a.jsx(b.Item,{label:"是否开启手机验证码",name:"enable_sms_code",valuePropName:"checked",extra:a.jsx(cu,{type:"secondary",style:{fontSize:12},children:"关闭后,注册仅支持邀请码,短信登录与忘记密码不可用"}),children:a.jsx(rt,{checkedChildren:"开启",unCheckedChildren:"关闭"})}),r&&a.jsx(b.Item,{label:"是否开启私域安全权限",name:"enable_security_perm",valuePropName:"checked",extra:a.jsx(cu,{type:"secondary",style:{fontSize:12},children:"开启后,本企业租户管理员在用户管理页可操作「开启/关闭安全权限」;仅平台超管可修改此开关"}),children:a.jsx(rt,{checkedChildren:"开启",unCheckedChildren:"关闭"})}),a.jsx(b.Item,{children:a.jsx(Y,{type:"primary",onClick:x,loading:i,disabled:n||!l||r&&l.tenant_id!==d,children:"保存设置"})})]})})})})},Dj=()=>a.jsx(rc,{messageTypes:["image","video","audio","file"]}),{Text:Dt}=Cr,Pj=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,[r,n]=g.useState(!1),[s,i]=g.useState(!1),[o,l]=g.useState(null),[c,d]=g.useState(void 0),u=g.useRef(0),[h]=b.useForm(),m=async x=>{const f=++u.current;n(!0);try{const p=x?{tenant_id:x}:void 0,I=await pa.getSettings(p);if(f!==u.current)return;(I.code===0||I.code===200)&&I.data?(l(I.data),h.setFieldsValue({...I.data,group_message_interval_sec:I.data.group_message_interval_sec??0,allow_voice_call:I.data.allow_voice_call??!0,allow_video_call:I.data.allow_video_call??!0,show_message_read_status:I.data.show_message_read_status??!1})):E.error(I.message||"获取安全策略失败")}catch(p){if(f!==u.current)return;E.error(p.message||"获取安全策略失败")}finally{f===u.current&&n(!1)}};g.useEffect(()=>{e!=null&&e.tenant_id&&m()},[e==null?void 0:e.tenant_id]);const _=async()=>{var x,f,p;if(t&&!c){E.error("请先选择租户并加载配置后再保存");return}try{const I=await h.validateFields();t||(delete I.allow_voice_call,delete I.allow_video_call),I.group_message_interval_sec!=null&&(I.group_message_interval_sec=Number(I.group_message_interval_sec)),i(!0);const S=c?{...I,tenant_id:c}:I,w=await pa.updateSettings(S);w.code===0||w.code===200?(E.success("安全策略已保存"),await m(c)):E.error(w.message||"保存失败")}catch(I){if((x=I==null?void 0:I.errorFields)!=null&&x.length){E.error(((p=(f=I.errorFields[0])==null?void 0:f.errors)==null?void 0:p[0])||"请检查表单填写");return}E.error(I.message||"保存失败")}finally{i(!1)}};return a.jsx("div",{className:"security-settings",children:a.jsx(Mn,{spinning:r,children:a.jsxs(b,{form:h,layout:"vertical",children:[t&&a.jsx(be,{size:"small",className:"security-card",children:a.jsx(b.Item,{label:"查询租户",style:{marginBottom:0},children:a.jsx(ot,{value:c,placeholder:"请选择租户",allowClear:!0,disabled:s,style:{width:240},onChange:x=>{u.current+=1,d(typeof x=="number"?x:void 0),l(null),h.resetFields(),typeof x=="number"?m(x):n(!1)}})})}),a.jsx(be,{title:"用户安全",className:"security-card",children:a.jsxs("div",{className:"security-form-grid",children:[a.jsx(b.Item,{label:"验证码触发次数",name:"user_captcha_threshold",rules:[{required:!0,message:"请输入"}],extra:"连续密码错误该次数后要求验证码(1~10)",children:a.jsx(at,{min:1,max:10,style:{width:"100%"}})}),a.jsx(b.Item,{label:"密码最小位数",name:"password_min_length",rules:[{required:!0,message:"请输入"}],extra:"用户密码最少字符数(6~32)",children:a.jsx(at,{min:6,max:32,style:{width:"100%"}})}),a.jsxs("div",{className:"switch-row",style:{alignItems:"center",padding:"8px 0"},children:[a.jsxs("div",{children:[a.jsx("div",{children:"强制包含字母"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"开启后密码必须包含英文字母"})]}),a.jsx(b.Item,{name:"password_require_letter",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]})]})}),a.jsxs("div",{className:"security-switch-grid",children:[a.jsx(be,{title:"注册登录管控",className:"security-card",size:"small",children:a.jsxs("div",{className:"switch-list",children:[a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"用户注册功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后用户端将隐藏注册入口"})]}),a.jsx(b.Item,{name:"allow_user_register",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"修改密码功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后用户无法修改密码,不影响后台重置"})]}),a.jsx(b.Item,{name:"allow_change_password",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"用户登录功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后用户无法登录,不影响已登录用户"})]}),a.jsx(b.Item,{name:"allow_user_login",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"Web 端多端登录"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"开启后同一账号可在多个浏览器标签页同时登录"})]}),a.jsx(b.Item,{name:"allow_web_multi_session",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]})]})}),a.jsx(be,{title:"聊天消息管控",className:"security-card",size:"small",children:a.jsxs("div",{className:"switch-list",children:[a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"全局好友功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后无法搜索和添加好友"})]}),a.jsx(b.Item,{name:"allow_friend_function",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"全局群消息"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后所有群无法发送消息"})]}),a.jsx(b.Item,{name:"allow_group_message",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"全局私聊消息"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后所有私聊无法发送消息"})]}),a.jsx(b.Item,{name:"allow_private_message",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"群聊发言间隔(秒)"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"0 表示不限制;1~1000 表示同一群内两次发言的最小间隔"})]}),a.jsx(b.Item,{name:"group_message_interval_sec",initialValue:0,style:{marginBottom:0},rules:[{validator:(x,f)=>{if(f==null||f==="")return Promise.reject(new Error("请输入群聊发言间隔"));const p=Number(f);return!Number.isFinite(p)||p<0||p>1e3?Promise.reject(new Error("范围为 0~1000 秒")):Promise.resolve()}}],children:a.jsx(at,{min:0,max:1e3,precision:0,style:{width:200}})})]})]})}),a.jsx(be,{title:"群组功能管控",className:"security-card",size:"small",children:a.jsxs("div",{className:"switch-list",children:[a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"群号搜索功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后用户端隐藏群号和群搜索入口"})]}),a.jsx(b.Item,{name:"allow_group_search",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"群创建功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后任何人无法新建群聊"})]}),a.jsx(b.Item,{name:"allow_group_create",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"群解散功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后任何人无法解散群聊,不影响后台管理员"})]}),a.jsx(b.Item,{name:"allow_group_dissolve",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"群管理员功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后任何人无法设置群管理员"})]}),a.jsx(b.Item,{name:"allow_group_admin",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"是否显示群人数"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后用户端查看群资料时不显示群总人数"})]}),a.jsx(b.Item,{name:"show_group_member_count",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]})]})}),a.jsx(be,{title:"其他功能开关",className:"security-card",size:"small",children:a.jsxs("div",{className:"switch-list",children:[a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"允许用户注销账号"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后用户端将隐藏注销账号按钮"})]}),a.jsx(b.Item,{name:"allow_user_deactivate",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),t&&a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"加密推送"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"开启后消息推送内容将加密(通话推送除外)"})]}),a.jsx(b.Item,{name:"push_encrypt_enabled",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]})]})})]}),a.jsxs(be,{title:"IP 访问控制",className:"security-card security-card-call",size:"small",children:[a.jsx(b.Item,{label:"注册 IP 黑名单",name:"register_ip_blacklist",extra:"留空表示不限制;每行一个 IPv4、IPv6 或 CIDR。命中后无法注册及获取注册短信验证码。",children:a.jsx(ve.TextArea,{rows:4,placeholder:`203.0.113.10 +10.0.0.0/8`})}),a.jsx(b.Item,{label:"登录 IP 黑名单",name:"login_ip_blacklist",extra:"留空表示不限制;命中后无法密码登录、短信登录及刷新 Token。与用户分组登录白名单独立,黑名单优先。",style:{marginBottom:0},children:a.jsx(ve.TextArea,{rows:4,placeholder:"198.51.100.0/24"})})]}),a.jsx(be,{title:t?"通话与高级功能":"高级功能",className:"security-card security-card-call",size:"small",children:a.jsxs("div",{className:"switch-list",children:[t&&a.jsxs(a.Fragment,{children:[a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"语音通话功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后用户端隐藏语音通话按钮"})]}),a.jsx(b.Item,{name:"allow_voice_call",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"视频通话功能"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"关闭后用户端隐藏视频通话按钮"})]}),a.jsx(b.Item,{name:"allow_video_call",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]})]}),a.jsxs("div",{className:"switch-row",children:[a.jsxs("div",{children:[a.jsx("div",{children:"消息已读状态"}),a.jsx(Dt,{type:"secondary",style:{fontSize:12},children:"开启后显示消息已读/未读标记"})]}),a.jsx(b.Item,{name:"show_message_read_status",valuePropName:"checked",noStyle:!0,children:a.jsx(rt,{size:"small"})})]})]})}),t&&a.jsx(be,{title:"管理员登录安全",className:"security-card",children:a.jsxs("div",{className:"security-form-grid",children:[a.jsx(b.Item,{label:"密码错误锁定次数",name:"admin_max_login_attempts",rules:[{required:!0,message:"请输入"}],extra:"范围 2~10",children:a.jsx(at,{min:2,max:10,style:{width:"100%"}})}),a.jsx(b.Item,{label:"锁定时长(分钟)",name:"admin_lockout_duration",rules:[{required:!0,message:"请输入"}],extra:"范围 5~1440",children:a.jsx(at,{min:5,max:1440,style:{width:"100%"}})})]})}),a.jsx("div",{className:"security-save",children:a.jsx(Y,{type:"primary",size:"large",onClick:_,loading:s,disabled:r||!o,children:"保存设置"})})]})})})},Nj={password_error:{label:"密码错误",color:"red"},locked:{label:"账户锁定",color:"volcano"},disabled:{label:"账户禁用",color:"orange"},captcha_failed:{label:"验证码错误",color:"gold"},tenant_banned:{label:"私域封禁",color:"magenta"}},Lj={user:{label:"用户",color:"blue"},admin:{label:"管理员",color:"purple"}},Mj=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,[r,n]=g.useState(!1),[s,i]=g.useState([]),[o,l]=g.useState(1),[c,d]=g.useState(20),[u,h]=g.useState(0),[m,_]=g.useState({}),[x]=b.useForm(),f=async(v=o,y=c,R=m)=>{n(!0);try{const j=await pa.listFailureLogs({...R,page:v,page_size:y});(j.code===0||j.code===200)&&j.data?(i(j.data.items||[]),h(j.data.total),l(j.data.page),d(j.data.size)):E.error(j.message||"获取登录失败日志失败")}catch(j){E.error(j.message||"获取登录失败日志失败")}finally{n(!1)}};g.useEffect(()=>{f(1,c,m)},[]);const p=v=>{const y={};return v.tenant_id&&(y.tenant_id=Number(v.tenant_id)),v.target_type&&(y.target_type=v.target_type),v.login_account&&(y.login_account=v.login_account.trim()),v.ip&&(y.ip=v.ip.trim()),v.startTime&&(y.start_time=v.startTime.format("YYYY-MM-DD HH:mm:ss")),v.endTime&&(y.end_time=v.endTime.format("YYYY-MM-DD HH:mm:ss")),y},I=async v=>{try{const y=await pa.unlockAccount({tenant_id:v.tenant_id>0?v.tenant_id:void 0,target_type:v.target_type,login_account:v.login_account});y.code===0||y.code===200?E.success(`已解锁账号 ${v.login_account}`):E.error(y.message||"解锁失败")}catch(y){E.error(y.message||"解锁失败")}},S=v=>{const y=p(v);_(y),f(1,c,y)},w=[...t?[{title:"ID",dataIndex:"id",width:80}]:[],...t?[{title:"所属私域",dataIndex:"tenant_name",width:140,render:(v,y)=>y.tenant_id>0?`${v} (${y.tenant_id})`:"-"}]:[],{title:"类型",dataIndex:"target_type",width:90,render:v=>{const y=Lj[v];return y?a.jsx(Te,{color:y.color,children:y.label}):v}},{title:"登录账号",dataIndex:"login_account",width:160},{title:"失败原因",dataIndex:"failure_reason",width:120,render:v=>{const y=Nj[v];return y?a.jsx(Te,{color:y.color,children:y.label}):a.jsx(Te,{children:v})}},{title:"IP 地址",dataIndex:"ip",width:140},{title:"User-Agent",dataIndex:"user_agent",width:200,ellipsis:!0,render:v=>a.jsx(Xt,{title:v,children:a.jsx("span",{children:v})})},{title:"时间",dataIndex:"created_at",width:180,render:v=>Ke(v)},{title:"操作",key:"action",width:100,render:(v,y)=>y.failure_reason==="locked"||y.failure_reason==="password_error"?a.jsx(js,{title:"确认解锁",description:`解锁账号 ${y.login_account} 的登录锁定?`,onConfirm:()=>I(y),okText:"确认",cancelText:"取消",children:a.jsx(Y,{type:"link",size:"small",icon:a.jsx(Xo,{}),children:"解锁"})}):null}];return a.jsxs("div",{className:"login-failure-logs",children:[a.jsx(be,{className:"login-failure-logs-filter",children:a.jsxs(b,{form:x,layout:"inline",onFinish:S,children:[t&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"类型",name:"target_type",children:a.jsxs($e,{placeholder:"全部",allowClear:!0,style:{width:120},children:[a.jsx($e.Option,{value:"user",children:"用户"}),a.jsx($e.Option,{value:"admin",children:"管理员"})]})}),a.jsx(b.Item,{label:"登录账号",name:"login_account",children:a.jsx(ve,{placeholder:"可选",style:{width:150}})}),a.jsx(b.Item,{label:"IP",name:"ip",children:a.jsx(ve,{placeholder:"可选",style:{width:140}})}),a.jsx(b.Item,{label:"开始时间",name:"startTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{label:"结束时间",name:"endTime",children:a.jsx(jr,{showTime:!0,placeholder:"可选",style:{width:200}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:()=>x.resetFields(),children:"重置"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"id",columns:w,dataSource:s,loading:r,scroll:{x:t?1400:1200},pagination:{current:o,pageSize:c,total:u,showSizeChanger:!0,onChange:(v,y)=>{l(v),d(y),f(v,y)}}})})]})},{Text:Pt,Paragraph:Bj}=Cr,{TextArea:li}=ve,Uj=()=>{const[e,t]=g.useState([""]),[r,n]=g.useState(""),[s,i]=g.useState(""),[o,l]=g.useState(!1),[c,d]=g.useState(""),[u,h]=g.useState(""),[m,_]=g.useState(167),[x,f]=g.useState(""),[p,I]=g.useState(!1),[S,w]=g.useState(""),[v,y]=g.useState(167),[R,j]=g.useState(""),[T,C]=g.useState(""),[F,P]=g.useState([]),[V,X]=g.useState([]),[_e,B]=g.useState(!0),[te,J]=g.useState(!1),[G,se]=g.useState(!1),[Q,de]=g.useState(!1),[k,W]=g.useState([""]),[z,D]=g.useState(""),[q,K]=g.useState(""),[Z,me]=g.useState(null),[ie,he]=g.useState(!1);g.useEffect(()=>{we()},[]);const we=async()=>{B(!0);try{const xe=await Un.getConfig();(xe.code===0||xe.code===200)&&xe.data&&(C(xe.data.aes_key||""),P(xe.data.records||[]),X(xe.data.encrypted_records||[]),xe.data.aes_key&&(n(xe.data.aes_key),K(xe.data.aes_key)))}catch{}finally{B(!1)}},He=async(xe,ke,et)=>{J(!0);try{const _t=await Un.saveConfig({aes_key:xe,records:ke,encrypted_records:et??V});return _t.code===0||_t.code===200?(C(xe),P(ke),et&&X(et),!0):(E.error(_t.message||"保存失败"),!1)}catch(_t){return E.error((_t==null?void 0:_t.message)||"保存请求失败"),!1}finally{J(!1)}},ze=()=>{if(e.length>=10){E.warning("最多支持 10 个域名");return}t([...e,""])},gt=xe=>{e.length<=1||t(e.filter((ke,et)=>et!==xe))},qe=(xe,ke)=>{const et=[...e];et[xe]=ke.trim(),t(et)},le=()=>{const xe=new Uint8Array(32);crypto.getRandomValues(xe);const ke=Array.from(xe).map(et=>et.toString(16).padStart(2,"0")).join("");n(ke)},fe=async()=>{if(r.length!==64){E.error("密钥必须为 64 位 hex");return}await He(r,F)&&E.success("密钥已保存")},Re=()=>{const xe=e.filter(ke=>ke.length>0);if(xe.length===0){E.error("请至少输入一个域名");return}if(r.length!==64){E.error("请先设置 64 位 hex AES 密钥,用于加密推送内容");return}Me.confirm({title:"确认推送域名更新(加密)",content:`将使用 AES-256-GCM 加密后,通过极光推送向所有在线客户端下发 ${xe.length} 个域名。客户端收到后解密并切换 API 地址。确认推送?`,okText:"确认推送",cancelText:"取消",onOk:async()=>{se(!0);try{const ke=await Un.pushUpdate(xe,r);ke.code===0||ke.code===200?E.success(`已加密推送 ${xe.length} 个域名到所有客户端`):E.error(ke.message||"推送失败")}catch(ke){E.error((ke==null?void 0:ke.message)||"推送请求失败")}finally{se(!1)}}})},Ue=()=>{const xe=k.filter(ke=>ke.length>0);if(xe.length===0){E.error("请至少输入一个桶地址");return}if(r.length!==64){E.error("请先设置 64 位 hex AES 密钥,用于加密推送内容");return}Me.confirm({title:"确认推送桶地址更新(加密)",content:`将使用 AES-256-GCM 加密后,通过极光推送向所有客户端下发 ${xe.length} 个桶地址。客户端收到后解密并缓存,下次域名解析时使用新桶地址。确认推送?`,okText:"确认推送",cancelText:"取消",onOk:async()=>{de(!0);try{const ke=await Un.pushBucketUpdate(xe,r);ke.code===0||ke.code===200?E.success(`已加密推送 ${xe.length} 个桶地址到所有客户端`):E.error(ke.message||"推送失败")}catch(ke){E.error((ke==null?void 0:ke.message)||"推送请求失败")}finally{de(!1)}}})},nt=async()=>{const xe=e.filter(ke=>ke.length>0);if(xe.length===0){E.error("请至少输入一个域名");return}if(r.length!==64){E.error("密钥必须为 64 位 hex 字符串(32 字节 AES-256)");return}l(!0);try{const ke=await Un.encrypt({domains:xe,key:r});if((ke.code===0||ke.code===200)&&ke.data){i(ke.data.encrypted);const _t={domains:xe,encrypted:ke.data.encrypted,key_used:r.slice(0,8)+"..."+r.slice(-8),created_at:new Date().toLocaleString()},Wt=[...V,_t];await He(T,F,Wt)?E.success("加密成功,已自动保存记录"):E.success("加密成功")}else E.error(ke.message||"加密失败")}catch(ke){E.error((ke==null?void 0:ke.message)||"加密请求失败")}finally{l(!1)}},Tt=async()=>{if(!z.trim()){E.error("请输入加密内容");return}if(q.length!==64){E.error("密钥必须为 64 位 hex");return}he(!0);try{const xe=await Un.decrypt({encrypted:z.trim(),key:q});(xe.code===0||xe.code===200)&&xe.data?(me(xe.data.domains),E.success("解密成功")):E.error(xe.message||"解密失败")}catch(xe){E.error((xe==null?void 0:xe.message)||"解密失败:密钥不匹配或密文损坏")}finally{he(!1)}},yt=async xe=>{const ke=V.filter((_t,Wt)=>Wt!==xe);await He(T,F,ke)&&E.success("已删除")},De=async()=>{if(!c.trim()){E.error("请输入要混淆的字符串");return}I(!0);try{const xe=await Un.obfuscate({text:c.trim(),xor_key:m});(xe.code===0||xe.code===200)&&xe.data?(f(xe.data.byte_array),E.success("生成成功")):E.error(xe.message||"生成失败")}catch(xe){E.error((xe==null?void 0:xe.message)||"请求失败")}finally{I(!1)}},Ce=async()=>{if(!x){E.error("请先生成混淆结果");return}const xe={label:u.trim()||c.trim().slice(0,30),text:c.trim(),byte_array:x,xor_key:m,created_at:new Date().toLocaleString()},ke=[...F,xe];await He(T,ke)&&E.success("已保存")},Qe=async xe=>{const ke=F.filter((_t,Wt)=>Wt!==xe);await He(T,ke)&&E.success("已删除")},Xe=()=>{try{const xe=S.match(/byteArrayOf\((.+)\)/),et=(xe?xe[1]:S).split(",").map(Wt=>parseInt(Wt.trim(),10));if(et.some(isNaN)){E.error("格式错误,请输入 byteArrayOf(...) 或逗号分隔的数字");return}const _t=et.map(Wt=>String.fromCharCode(Wt&255^v&255)).join("");j(_t)}catch{E.error("解码失败")}},Je=xe=>{if(navigator.clipboard&&window.isSecureContext)navigator.clipboard.writeText(xe).then(()=>E.success("已复制到剪贴板"));else{const ke=document.createElement("textarea");ke.value=xe,ke.style.position="fixed",ke.style.opacity="0",document.body.appendChild(ke),ke.select(),document.execCommand("copy"),document.body.removeChild(ke),E.success("已复制到剪贴板")}};return _e?a.jsx("div",{style:{padding:24,textAlign:"center"},children:a.jsx(Mn,{tip:"加载配置中..."})}):a.jsxs("div",{style:{padding:24},children:[a.jsx(Zs,{message:"域名加密工具",description:"输入 API 域名列表并加密,将加密内容手动上传到阿里云 OSS / 腾讯云 COS 存储桶。App 启动时从存储桶获取并解密,按顺序检测连通性后使用。",type:"info",showIcon:!0,style:{marginBottom:16}}),a.jsxs(Es,{gutter:16,style:{marginBottom:16},align:"stretch",children:[a.jsx(Ct,{span:12,style:{display:"flex"},children:a.jsx(be,{title:a.jsxs(a.Fragment,{children:[a.jsx(xi,{style:{marginRight:8}}),"AES-256 密钥"]}),size:"small",style:{flex:1},children:a.jsxs(Ie,{direction:"vertical",style:{width:"100%"},size:"small",children:[a.jsxs(Ie.Compact,{style:{width:"100%"},children:[a.jsx(ve,{value:r,onChange:xe=>n(xe.target.value.trim()),placeholder:"64 位 hex 字符串(32 字节 AES-256-GCM 密钥)",maxLength:64,style:{fontFamily:"monospace"}}),a.jsx(Y,{onClick:le,children:"随机生成"}),a.jsx(Y,{icon:a.jsx(Js,{}),onClick:fe,loading:te,children:"保存"}),r&&a.jsx(Y,{icon:a.jsx(un,{}),onClick:()=>Je(r)})]}),T&&a.jsxs(Pt,{type:"secondary",children:["已保存: ",a.jsx(Pt,{code:!0,copyable:!0,style:{fontSize:12},children:T})]}),a.jsx(Pt,{type:"secondary",children:"此密钥需硬编码在 App 客户端中用于解密,请妥善保管"})]})})}),a.jsx(Ct,{span:12,style:{display:"flex"},children:a.jsx(be,{title:a.jsxs(a.Fragment,{children:[a.jsx(pi,{style:{marginRight:8}}),"域名列表"]}),size:"small",extra:a.jsxs(Ie,{children:[a.jsx(Y,{type:"dashed",size:"small",icon:a.jsx(ua,{}),onClick:ze,children:"添加"}),a.jsx(Y,{type:"primary",size:"small",loading:o,onClick:nt,children:"加密生成"}),a.jsx(Y,{size:"small",icon:a.jsx(uc,{}),loading:G,onClick:Re,children:"推送下发"})]}),style:{flex:1},children:a.jsxs(Ie,{direction:"vertical",style:{width:"100%"},size:"small",children:[e.map((xe,ke)=>a.jsxs(Ie,{style:{width:"100%"},children:[a.jsx(Te,{color:"blue",style:{minWidth:24,textAlign:"center"},children:ke+1}),a.jsx(ve,{value:xe,onChange:et=>qe(ke,et.target.value),placeholder:"例: api.example.com",style:{flex:1,minWidth:200}}),e.length>1&&a.jsx(Y,{icon:a.jsx(Gs,{}),danger:!0,size:"small",onClick:()=>gt(ke)})]},ke)),a.jsx(Pt,{type:"secondary",children:"App 按此顺序检测连通性,返回 HTTP 200 即使用该域名"})]})})})]}),a.jsx(be,{title:a.jsxs(a.Fragment,{children:[a.jsx(am,{style:{marginRight:8}}),"OSS 桶地址推送"]}),size:"small",extra:a.jsxs(Ie,{children:[a.jsx(Y,{type:"dashed",size:"small",icon:a.jsx(ua,{}),onClick:()=>{if(k.length>=5){E.warning("最多支持 5 个桶地址");return}W([...k,""])},children:"添加"}),a.jsx(Y,{size:"small",icon:a.jsx(uc,{}),loading:Q,onClick:Ue,children:"推送下发"})]}),style:{marginBottom:16},children:a.jsxs(Ie,{direction:"vertical",style:{width:"100%"},size:"small",children:[k.map((xe,ke)=>a.jsxs(Ie,{style:{width:"100%"},children:[a.jsx(Te,{color:"orange",style:{minWidth:24,textAlign:"center"},children:ke+1}),a.jsx(ve,{value:xe,onChange:et=>{const _t=[...k];_t[ke]=et.target.value.trim(),W(_t)},placeholder:"例: https://bucket.oss-accelerate.aliyuncs.com/config.json",style:{flex:1,minWidth:400}}),k.length>1&&a.jsx(Y,{icon:a.jsx(Gs,{}),danger:!0,size:"small",onClick:()=>W(k.filter((et,_t)=>_t!==ke))})]},ke)),a.jsx(Pt,{type:"secondary",children:"推送新桶地址到客户端,客户端缓存后优先使用新地址拉取域名配置(硬编码地址作为兜底)"})]})}),s&&a.jsxs(be,{title:"加密结果",size:"small",extra:a.jsx(Y,{size:"small",icon:a.jsx(un,{}),onClick:()=>Je(s),children:"复制"}),style:{marginBottom:16},children:[a.jsx(li,{value:s,readOnly:!0,rows:4,style:{fontFamily:"monospace",fontSize:12}}),a.jsx(Bj,{type:"secondary",style:{marginTop:8,marginBottom:0},children:"将此内容粘贴到存储桶文件中(阿里云 OSS + 腾讯云 COS 各放一份)"})]}),a.jsx(be,{title:a.jsxs(a.Fragment,{children:[a.jsx(pi,{style:{marginRight:8}}),"已保存的加密记录",a.jsx(Te,{color:"blue",style:{marginLeft:8},children:V.length})]}),size:"small",style:{marginBottom:16},children:V.length===0?a.jsx(Pt,{type:"secondary",children:"暂无加密记录,加密域名后将自动保存到此处"}):a.jsx(Ye,{dataSource:V.map((xe,ke)=>({...xe,_index:ke})),rowKey:"_index",pagination:!1,size:"small",columns:[{title:"域名列表",dataIndex:"domains",width:250,render:xe=>a.jsx(Xt,{title:xe==null?void 0:xe.join(", "),children:a.jsx(Pt,{style:{fontSize:12},children:xe==null?void 0:xe.join(", ")})}),ellipsis:!0},{title:"加密内容",dataIndex:"encrypted",ellipsis:!0,render:xe=>a.jsxs(Ie,{size:4,children:[a.jsx(Xt,{title:xe,children:a.jsx(Pt,{code:!0,style:{fontSize:11,maxWidth:300,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",display:"inline-block",verticalAlign:"middle"},children:xe})}),a.jsx(Y,{type:"text",size:"small",icon:a.jsx(un,{}),onClick:()=>Je(xe),style:{padding:"0 2px",minWidth:24}})]})},{title:"使用密钥",dataIndex:"key_used",width:180,render:xe=>a.jsxs(Ie,{size:4,children:[a.jsx(Pt,{code:!0,style:{fontSize:11},children:xe}),a.jsx(Y,{type:"text",size:"small",icon:a.jsx(un,{}),onClick:()=>Je(xe),style:{padding:"0 2px",minWidth:24}})]})},{title:"时间",dataIndex:"created_at",width:155,ellipsis:!0},{title:"操作",width:90,render:(xe,ke)=>a.jsxs(Ie,{size:"small",children:[a.jsx(Y,{size:"small",icon:a.jsx(xl,{}),onClick:()=>{D(ke.encrypted),T&&K(T),me(null)}}),a.jsx(Y,{size:"small",danger:!0,icon:a.jsx(Gs,{}),onClick:()=>yt(ke._index)})]})}]})}),a.jsxs(Es,{gutter:16,style:{marginBottom:16},align:"stretch",children:[a.jsx(Ct,{span:12,style:{display:"flex"},children:a.jsx(be,{title:a.jsxs(a.Fragment,{children:[a.jsx(Xo,{style:{marginRight:8}}),"域名列表解密"]}),size:"small",style:{flex:1},children:a.jsxs(Ie,{direction:"vertical",style:{width:"100%"},size:"small",children:[a.jsx(li,{value:z,onChange:xe=>D(xe.target.value),placeholder:"粘贴加密后的 base64 内容",rows:3,style:{fontFamily:"monospace",fontSize:12}}),a.jsxs(Ie,{children:[a.jsx(Pt,{children:"密钥:"}),a.jsx(ve,{value:q,onChange:xe=>K(xe.target.value.trim()),placeholder:"64 位 hex 密钥",maxLength:64,style:{flex:1,fontFamily:"monospace",fontSize:12}})]}),a.jsx(Y,{type:"primary",loading:ie,onClick:Tt,children:"解密域名列表"}),Z&&a.jsx(Zs,{type:"success",message:"解密结果",description:a.jsx(Ie,{direction:"vertical",size:2,children:Z.map((xe,ke)=>a.jsxs(Ie,{size:4,children:[a.jsx(Te,{color:"green",children:ke+1}),a.jsx(Pt,{code:!0,copyable:!0,children:xe})]},ke))})})]})})}),a.jsx(Ct,{span:12,style:{display:"flex"},children:a.jsxs(be,{title:a.jsxs(a.Fragment,{children:[a.jsx(rr,{style:{marginRight:8}}),"字符串混淆工具"]}),size:"small",style:{flex:1},children:[a.jsx(Zs,{message:"将密钥或桶地址转为 Kotlin byteArrayOf() 常量,防止反编译直接看到明文",type:"warning",style:{marginBottom:12,padding:"4px 12px"}}),a.jsxs(Ie,{direction:"vertical",style:{width:"100%"},size:"small",children:[a.jsx(ve,{value:u,onChange:xe=>h(xe.target.value),placeholder:"备注名称(如:阿里云 OSS 桶地址)"}),a.jsx(ve,{value:c,onChange:xe=>d(xe.target.value),placeholder:"输入要混淆的字符串(密钥、桶地址等)",style:{fontFamily:"monospace"}}),a.jsxs(Ie,{children:[a.jsx(Pt,{children:"XOR Key:"}),a.jsx(ve,{value:m,onChange:xe=>_(Number(xe.target.value)||0),style:{width:70,fontFamily:"monospace"},type:"number",min:0,max:255}),a.jsx(Pt,{type:"secondary",children:"(0~255,默认 0xA7)"})]}),a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",loading:p,onClick:De,children:"生成 byteArrayOf"}),x&&a.jsx(Y,{icon:a.jsx(Js,{}),onClick:Ce,loading:te,children:"保存记录"})]}),x&&a.jsxs(a.Fragment,{children:[a.jsx(li,{value:x,readOnly:!0,rows:2,style:{fontFamily:"monospace",fontSize:12}}),a.jsx(Y,{size:"small",icon:a.jsx(un,{}),onClick:()=>Je(x),children:"复制"})]})]})]})})]}),a.jsx(be,{title:a.jsxs(a.Fragment,{children:[a.jsx(Js,{style:{marginRight:8}}),"已保存的混淆记录",a.jsx(Te,{color:"blue",style:{marginLeft:8},children:F.length})]}),size:"small",children:F.length===0?a.jsx(Pt,{type:"secondary",children:'暂无混淆记录,生成 byteArrayOf 后点击"保存记录"将保存到此处'}):a.jsx(Ye,{dataSource:F.map((xe,ke)=>({...xe,_index:ke})),rowKey:"_index",pagination:!1,size:"small",columns:[{title:"备注",dataIndex:"label",width:140,ellipsis:!0},{title:"原始字符串",dataIndex:"text",width:280,render:xe=>a.jsxs(Ie,{size:4,children:[a.jsx(Xt,{title:xe,children:a.jsx(Pt,{code:!0,style:{fontSize:11,maxWidth:220,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",display:"inline-block",verticalAlign:"middle"},children:xe})}),a.jsx(Y,{type:"text",size:"small",icon:a.jsx(un,{}),onClick:()=>Je(xe),style:{padding:"0 2px",minWidth:24}})]})},{title:"byteArrayOf",dataIndex:"byte_array",ellipsis:!0,render:xe=>a.jsxs(Ie,{size:4,style:{flexWrap:"nowrap"},children:[a.jsx(Xt,{title:xe,overlayStyle:{maxWidth:600},children:a.jsx(Pt,{code:!0,style:{fontSize:11,maxWidth:350,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",display:"inline-block",verticalAlign:"middle"},children:xe})}),a.jsx(Y,{type:"link",size:"small",icon:a.jsx(un,{}),onClick:()=>Je(xe),style:{flexShrink:0},children:"复制"})]})},{title:"XOR",dataIndex:"xor_key",width:55},{title:"时间",dataIndex:"created_at",width:155,ellipsis:!0},{title:"操作",width:60,render:(xe,ke)=>a.jsx(Y,{size:"small",danger:!0,icon:a.jsx(Gs,{}),onClick:()=>Qe(ke._index)})}]})}),a.jsx(be,{title:a.jsxs(a.Fragment,{children:[a.jsx(Xo,{style:{marginRight:8}}),"XOR 解密验证"]}),size:"small",style:{marginTop:16},children:a.jsxs(Ie,{direction:"vertical",style:{width:"100%"},size:"small",children:[a.jsx(li,{value:S,onChange:xe=>w(xe.target.value),placeholder:"粘贴 byteArrayOf(-61, -106, ...) 或逗号分隔的数字",rows:2,style:{fontFamily:"monospace",fontSize:12}}),a.jsxs(Ie,{children:[a.jsx(Pt,{children:"XOR Key:"}),a.jsx(ve,{value:v,onChange:xe=>y(Number(xe.target.value)||0),style:{width:70,fontFamily:"monospace"},type:"number",min:0,max:255}),a.jsx(Y,{type:"primary",onClick:Xe,children:"解密验证"}),R&&a.jsxs(a.Fragment,{children:[a.jsx(Pt,{children:"结果:"}),a.jsx(Pt,{code:!0,copyable:!0,style:{maxWidth:500,wordBreak:"break-all"},children:R})]})]})]})})]})},{Text:du}=Cr,zj=[{label:"待处理",value:0},{label:"已解决",value:2}],$j=[{label:"低",value:1},{label:"中",value:2},{label:"高",value:3},{label:"紧急",value:4}],uu={0:"orange",2:"green"},fu={0:"待处理",2:"已解决"},hu={1:"blue",2:"cyan",3:"orange",4:"red"},mu={1:"低",2:"中",3:"高",4:"紧急"},Wj=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,{renderTenantId:r}=Yr(),[n,s]=g.useState(!1),[i,o]=g.useState(!1),[l,c]=g.useState([]),[d,u]=g.useState(1),[h,m]=g.useState(20),[_,x]=g.useState(0),[f,p]=g.useState({}),[I,S]=g.useState([]),[w]=b.useForm(),[v,y]=g.useState(!1),[R,j]=g.useState(!1),[T,C]=g.useState(null),[F,P]=g.useState(!1),[V,X]=g.useState(null),[_e]=b.useForm(),[B,te]=g.useState(!1),[J,G]=g.useState(null),[se]=b.useForm(),Q=async(he=d,we=h,He=f)=>{s(!0);try{const ze=await ds.list({...He,page:he,page_size:we});(ze.code===0||ze.code===200)&&ze.data?(c(ze.data.items),x(ze.data.total),u(ze.data.page),m(ze.data.size)):E.error(ze.message||"获取反馈列表失败")}catch(ze){E.error(ze.message||"获取反馈列表失败")}finally{s(!1)}};g.useEffect(()=>{Q(1,h,f)},[]);const de=he=>{const we={};he.tenant_id&&(we.tenant_id=he.tenant_id),he.user_id&&(we.user_id=he.user_id),he.status!==void 0&&he.status!==null&&(we.status=he.status),he.priority&&(we.priority=he.priority),he.keyword&&(we.keyword=he.keyword),p(we),Q(1,h,we)},k=async he=>{C(null),y(!0),j(!0);try{const we=await ds.detail(he);(we.code===0||we.code===200)&&we.data?C(we.data):E.error(we.message||"获取反馈详情失败")}catch(we){E.error(we.message||"获取反馈详情失败")}finally{j(!1)}},W=he=>{X(he),_e.resetFields(),P(!0)},z=async()=>{if(V)try{const he=await _e.validateFields(),we=await ds.handle(V.id,{status:he.status,handle_comment:he.handle_comment||void 0});we.code===0||we.code===200?(E.success("反馈已处理"),P(!1),Q()):E.error(we.message||"处理失败")}catch(he){if(he!=null&&he.errorFields)return;E.error(he.message||"处理失败")}},D=he=>{G(he),se.setFieldsValue({priority:he.priority}),te(!0)},q=async()=>{if(J)try{const he=await se.validateFields(),we=await ds.updatePriority(J.id,{priority:he.priority});we.code===0||we.code===200?(E.success("优先级已更新"),te(!1),Q()):E.error(we.message||"更新失败")}catch(he){if(he!=null&&he.errorFields)return;E.error(he.message||"更新失败")}},K=async()=>{if(I.length===0){E.warning("请先选择要处理的反馈");return}try{const he=await ds.batchHandle({ids:I.map(He=>Number(He))});(he.code===0||he.code===200)&&he.data?(E.success(`批量处理完成:成功 ${he.data.success} 条,失败 ${he.data.failed} 条`),S([]),Q()):E.error(he.message||"批量处理失败")}catch(he){E.error(he.message||"批量处理失败")}},Z=async()=>{o(!0);try{const he=await ds.export(f),we=window.URL.createObjectURL(new Blob([he],{type:"text/csv"})),He=document.createElement("a");He.href=we,He.setAttribute("download",`feedbacks_${Date.now()}.csv`),document.body.appendChild(He),He.click(),He.remove(),window.URL.revokeObjectURL(we),E.success("导出成功")}catch(he){E.error(he.message||"导出失败")}finally{o(!1)}},me={selectedRowKeys:I,onChange:he=>S(he)},ie=[...t?[{title:"ID",dataIndex:"id",width:80}]:[],...t?[{title:"租户",dataIndex:"tenant_id",width:160,render:r}]:[],{title:"用户",dataIndex:"user_name",width:120},{title:"优先级",dataIndex:"priority",width:90,render:he=>a.jsx(Te,{color:hu[he]||"default",children:mu[he]||he})},{title:"内容",dataIndex:"content",width:280,render:he=>a.jsx(Xt,{title:he,children:a.jsx(du,{ellipsis:!0,style:{maxWidth:260},children:he})})},{title:"状态",dataIndex:"status",width:100,render:he=>a.jsx(Te,{color:uu[he]||"default",children:fu[he]||he})},{title:"处理人",dataIndex:"handler_name",width:120,render:he=>he||"--"},{title:"创建时间",dataIndex:"created_at",width:180,render:he=>Ke(he)},{title:"操作",key:"actions",width:200,fixed:"right",render:(he,we)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",onClick:()=>k(we.id),children:"详情"}),we.status===0&&a.jsx(Y,{size:"small",type:"primary",onClick:()=>W(we),children:"处理"}),a.jsx(Y,{size:"small",onClick:()=>D(we),children:"优先级"})]})}];return a.jsxs("div",{className:"admin-feedbacks",children:[a.jsx(be,{className:"admin-feedbacks-filter",children:a.jsxs(b,{form:w,layout:"inline",onFinish:de,children:[t&&a.jsx(b.Item,{label:"租户",name:"tenant_id",children:a.jsx(ot,{allowClear:!0,style:{width:200}})}),a.jsx(b.Item,{label:"用户ID",name:"user_id",children:a.jsx(ve,{placeholder:"用户ID",allowClear:!0,style:{width:140}})}),a.jsx(b.Item,{label:"状态",name:"status",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:zj,style:{width:120}})}),a.jsx(b.Item,{label:"优先级",name:"priority",children:a.jsx($e,{placeholder:"全部",allowClear:!0,options:$j,style:{width:120}})}),a.jsx(b.Item,{label:"关键词",name:"keyword",children:a.jsx(ve,{placeholder:"内容/联系方式",allowClear:!0,style:{width:180}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",children:"查询"}),a.jsx(Y,{onClick:()=>{w.resetFields(),de({})},children:"重置"})]})})]})}),a.jsxs(be,{children:[a.jsx("div",{style:{marginBottom:16},children:a.jsxs(Ie,{children:[a.jsx(js,{title:"确认批量处理",description:`将 ${I.length} 条反馈标记为已解决,是否继续?`,onConfirm:K,okText:"确认",cancelText:"取消",children:a.jsx(Y,{disabled:I.length===0,children:"批量处理"})}),a.jsx(Y,{icon:a.jsx($r,{}),loading:i,onClick:Z,children:"导出 CSV"})]})}),a.jsx(Ye,{rowKey:"id",rowSelection:me,columns:ie,dataSource:l,loading:n,scroll:{x:1400},pagination:{current:d,pageSize:h,total:_,showSizeChanger:!0,onChange:(he,we)=>{Q(he,we)}}})]}),a.jsx($i,{title:"反馈详情",width:520,open:v,onClose:()=>y(!1),children:a.jsx(Mn,{spinning:R,children:T&&a.jsxs(a.Fragment,{children:[a.jsxs(je,{column:1,bordered:!0,size:"small",children:[a.jsx(je.Item,{label:"ID",children:T.id}),a.jsx(je.Item,{label:"租户 ID",children:T.tenant_id}),a.jsxs(je.Item,{label:"用户",children:[T.user_name,"(ID: ",T.user_id,")"]}),a.jsx(je.Item,{label:"优先级",children:a.jsx(Te,{color:hu[T.priority]||"default",children:mu[T.priority]||T.priority})}),a.jsx(je.Item,{label:"内容",children:T.content}),a.jsx(je.Item,{label:"联系方式",children:T.contact_info||"--"}),a.jsx(je.Item,{label:"状态",children:a.jsx(Te,{color:uu[T.status]||"default",children:fu[T.status]||T.status})}),a.jsx(je.Item,{label:"处理人",children:T.handler_name||"--"}),a.jsx(je.Item,{label:"处理备注",children:T.handle_comment||"--"}),a.jsx(je.Item,{label:"处理时间",children:Ke(T.handle_time)}),a.jsx(je.Item,{label:"创建时间",children:Ke(T.created_at)}),a.jsx(je.Item,{label:"更新时间",children:Ke(T.updated_at)})]}),T.attachment_urls&&T.attachment_urls.length>0&&a.jsxs("div",{style:{marginTop:16},children:[a.jsx(du,{strong:!0,children:"附件:"}),a.jsx("div",{style:{marginTop:8},children:a.jsx(Mr.PreviewGroup,{children:T.attachment_urls.map((he,we)=>a.jsx(Mr,{src:he,className:"admin-feedbacks-attachment-img",fallback:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN8/+F9PQAI8wNPvd7POQAAAABJRU5ErkJggg=="},we))})})]})]})})}),a.jsx(Me,{title:"处理反馈",open:F,onCancel:()=>P(!1),onOk:z,okText:"确认",cancelText:"取消",destroyOnClose:!0,children:a.jsxs(b,{form:_e,layout:"vertical",children:[a.jsx(b.Item,{label:"处理结果",name:"status",rules:[{required:!0,message:"请选择处理结果"}],children:a.jsx($e,{placeholder:"选择处理结果",children:a.jsx($e.Option,{value:"resolved",children:"已解决"})})}),a.jsx(b.Item,{label:"处理备注",name:"handle_comment",children:a.jsx(ve.TextArea,{rows:3,placeholder:"可选备注",maxLength:1e3,showCount:!0})})]})}),a.jsx(Me,{title:"更新优先级",open:B,onCancel:()=>te(!1),onOk:q,okText:"确认",cancelText:"取消",destroyOnClose:!0,children:a.jsx(b,{form:se,layout:"vertical",children:a.jsx(b.Item,{label:"优先级",name:"priority",rules:[{required:!0,message:"请选择优先级"}],children:a.jsxs($e,{placeholder:"选择优先级",children:[a.jsx($e.Option,{value:1,children:"低"}),a.jsx($e.Option,{value:2,children:"中"}),a.jsx($e.Option,{value:3,children:"高"}),a.jsx($e.Option,{value:4,children:"紧急"})]})})})})]})},{Title:Hj,Text:Hs,Paragraph:xu}=Cr,Vj=()=>{const e="ABCDEFGHIJKLMNOPQRSTUVWXYZ",t="abcdefghijklmnopqrstuvwxyz",r="0123456789",n=e+t+r,s=[e[Math.floor(Math.random()*e.length)],t[Math.floor(Math.random()*t.length)],r[Math.floor(Math.random()*r.length)]];for(let i=0;i<9;i++)s.push(n[Math.floor(Math.random()*n.length)]);for(let i=s.length-1;i>0;i--){const o=Math.floor(Math.random()*(i+1));[s[i],s[o]]=[s[o],s[i]]}return s.join("")},pu=async()=>{const[e,t]=await Promise.all([fetch("/前缀.txt").then(r=>r.text()),fetch("/后缀.txt").then(r=>r.text())]);return{prefixes:e.split(` +`).map(r=>r.trim()).filter(Boolean),suffixes:t.split(` +`).map(r=>r.trim()).filter(Boolean)}},gu=(e,t,r)=>{const n=[];for(let s=0;s{const[e]=b.useForm(),{admin:t}=pt(),{renderTenantId:r}=Yr(),n=!!t&&!t.tenant_id,[s,i]=g.useState(!1),[o,l]=g.useState("new"),[c,d]=g.useState([]),[u,h]=g.useState(null),[m,_]=g.useState([]),[x,f]=g.useState(!1),p=b.useWatch("tenant_id",e),I=b.useWatch("member_count",e)??10,S=n?p:t==null?void 0:t.tenant_id,w=g.useCallback(async(T,C)=>{var F;f(!0);try{const P=await Lt.list({tenant_id:T,keyword:(C==null?void 0:C.trim())||void 0,page:1,page_size:50});(P.code===0||P.code===200)&&((F=P.data)!=null&&F.items)&&_(P.data.items.map(X=>({value:X.id,label:`${X.nickname||X.username} (${X.username}) #${X.id}`})))}finally{f(!1)}},[]);g.useEffect(()=>{o==="existing"&&S&&w(S)},[o,S,w]);const v=g.useMemo(()=>{const T=Number(I)||0;return o==="existing"?Math.max(0,T-1):T},[I,o]),y=async()=>{try{if(await e.validateFields(["tenant_id","member_count","owner_id"]),v<=0){E.warning("群人数至少为 2");return}const T=await pu();if(T.prefixes.length===0||T.suffixes.length===0){E.error("前缀或后缀文件为空");return}d(gu(v,T.prefixes,T.suffixes)),E.success(`已生成 ${v} 名成员预览`)}catch{}},R=T=>{const C=new Date,F=`${C.getFullYear()}-${String(C.getMonth()+1).padStart(2,"0")}-${String(C.getDate()).padStart(2,"0")} ${String(C.getHours()).padStart(2,"0")}:${String(C.getMinutes()).padStart(2,"0")}:${String(C.getSeconds()).padStart(2,"0")}`;let P=`快速建群 - 生成时间:${F} +`;P+=`群名称:${T.group_name} +`,P+=`群号:${T.group_code} +`,P+=`群 ID:${T.group_id} +`,P+=`群主 ID:${T.owner_id} +`,P+=`================================================ +`,P+=`昵称 用户名 密码 身份 +`,P+=`------------------------------------------------ +`;for(const B of T.members)P+=`${B.nickname} ${B.username} ${B.password} ${B.is_owner?"群主":"成员"} +`;const V=new Blob(["\uFEFF"+P],{type:"text/plain;charset=utf-8"}),X=URL.createObjectURL(V),_e=document.createElement("a");_e.href=X,_e.download=`快速建群_${T.group_code}_${F.replace(/[: ]/g,"-")}.txt`,document.body.appendChild(_e),_e.click(),document.body.removeChild(_e),URL.revokeObjectURL(X)},j=async()=>{var T,C;try{const F=await e.validateFields(),P=n?F.tenant_id:t==null?void 0:t.tenant_id;if(!P){E.error("无法获取企业信息");return}if(o==="existing"&&!F.owner_id){E.error("请选择群主");return}if(v<=0){E.error("群人数至少为 2");return}let V=c;if(V.length!==v){const _e=await pu();V=gu(v,_e.prefixes,_e.suffixes),d(V)}i(!0);const X=E.loading(`正在创建 ${v} 名用户并建群,人数较多可能需要 1~3 分钟,请勿关闭页面…`,0);try{const _e=await Wn.quickCreate({tenant_id:P,owner_id:o==="existing"?F.owner_id:void 0,member_count:F.member_count,group_name:((T=F.group_name)==null?void 0:T.trim())||void 0,users:V});(_e.code===0||_e.code===200)&&_e.data?(h(_e.data),E.success(`群组「${_e.data.group_name}」创建成功`),R(_e.data)):E.error(_e.message||"创建失败")}finally{X()}}catch(F){if(F!=null&&F.errorFields)return;if((C=F==null?void 0:F.message)!=null&&C.includes("timeout")){E.error("请求超时,若后台仍在处理请稍后到群组管理查看是否已创建成功");return}E.error(F.message||"创建失败")}finally{i(!1)}};return a.jsxs("div",{style:{padding:24},children:[a.jsx(Hj,{level:4,style:{marginTop:0},children:"快速建群"}),a.jsx(xu,{type:"secondary",children:"指定企业与群人数,一键批量新建用户并创建群组。可选择已有用户作为群主,或由第一个新建用户担任群主。"}),a.jsx(be,{style:{maxWidth:720,marginBottom:24},children:a.jsxs(b,{form:e,layout:"vertical",initialValues:{member_count:10,owner_mode:"new"},onValuesChange:T=>{"owner_mode"in T&&(l(T.owner_mode),d([]),h(null)),("member_count"in T||"tenant_id"in T)&&(d([]),h(null))},children:[n?a.jsx(b.Item,{label:"企业",name:"tenant_id",rules:[{required:!0,message:"请选择企业"}],children:a.jsx(ot,{placeholder:"请选择企业",style:{width:"100%"}})}):a.jsx(b.Item,{label:"企业",children:a.jsx(ve,{value:r(t==null?void 0:t.tenant_id),disabled:!0})}),a.jsx(b.Item,{label:"群名称",name:"group_name",extra:"留空则自动生成",children:a.jsx(ve,{placeholder:"例如:测试群 A",maxLength:100})}),a.jsx(b.Item,{label:"群人数(含群主)",name:"member_count",rules:[{required:!0,message:"请输入群人数"}],children:a.jsx(at,{min:2,max:500,style:{width:"100%"}})}),a.jsx(b.Item,{label:"群主",name:"owner_mode",children:a.jsxs(_o.Group,{children:[a.jsx(_o,{value:"new",children:"新建用户担任群主(第一个新建用户)"}),a.jsx(_o,{value:"existing",children:"指定已有用户为群主"})]})}),o==="existing"&&a.jsx(b.Item,{label:"选择群主",name:"owner_id",rules:[{required:!0,message:"请选择群主"}],children:a.jsx($e,{showSearch:!0,placeholder:"搜索用户名或昵称",optionFilterProp:"label",loading:x,options:m,onSearch:T=>{S&&w(S,T)},disabled:!S})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsxs(Y,{onClick:y,disabled:!S,children:["预览成员(",v," 人)"]}),a.jsx(Y,{type:"primary",loading:s,onClick:j,children:"一键建群"})]})}),a.jsxs(Hs,{type:"secondary",children:["将新建 ",v," 名用户",o==="existing"?",并使用所选用户作为群主":",其中第一个用户为群主"]})]})}),c.length>0&&a.jsx(be,{title:`成员预览(${c.length} 人)`,style:{marginBottom:24},children:a.jsx(Ye,{rowKey:(T,C)=>String(C),size:"small",pagination:!1,dataSource:c.map((T,C)=>({...T,key:C,role:o==="new"&&C===0?"群主":"成员"})),columns:[{title:"#",width:60,render:(T,C,F)=>F+1},{title:"昵称",dataIndex:"nickname"},{title:"密码",dataIndex:"password",render:T=>a.jsx(Hs,{code:!0,copyable:{text:T},children:T})},{title:"身份",dataIndex:"role",width:80}]})}),u&&a.jsxs(be,{title:"创建结果",extra:u?a.jsx(Y,{onClick:()=>R(u),children:"下载账号 TXT"}):void 0,children:[a.jsxs(xu,{children:["群名称:",a.jsx(Hs,{strong:!0,children:u.group_name})," · ","群号:",a.jsx(Hs,{code:!0,children:u.group_code})," · ","群 ID:",u.group_id]}),a.jsx(Ye,{rowKey:"id",size:"small",dataSource:u.members,columns:[{title:"ID",dataIndex:"id",width:80},{title:"昵称",dataIndex:"nickname"},{title:"用户名",dataIndex:"username"},{title:"密码",dataIndex:"password",render:T=>a.jsx(Hs,{code:!0,copyable:{text:T},children:T})},{title:"身份",dataIndex:"is_owner",width:80,render:T=>T?"群主":"成员"}],pagination:!1})]})]})},_u={name:"",is_default:!1,allow_add_friend:!0,allow_create_group:!0,direct_add_friend:!1,max_friend_count:0,allow_app_login:!0,allow_web_login:!0,ip_whitelist_enabled:!1,enabled:!0,sort_order:0},Gj=()=>{var X,_e;const{admin:e}=pt(),t=!(e!=null&&e.tenant_id),[r,n]=g.useState(e==null?void 0:e.tenant_id),[s,i]=g.useState([]),[o,l]=g.useState(!1),[c,d]=g.useState(!1),[u,h]=g.useState(null),[m,_]=g.useState(!1),[x,f]=g.useState(!1),[p,I]=g.useState(null),[S]=b.useForm(),[w]=b.useForm(),v=async(B=r)=>{if(!B){i([]),I(null);return}l(!0);try{const[te,J]=await Promise.all([Fn.list(t?B:void 0),Fn.getIPWhitelist(t?B:void 0)]);(te.code===0||te.code===200)&&te.data?i(te.data):E.error(te.message||"获取用户分组失败"),(J.code===0||J.code===200)&&J.data&&I(J.data)}catch(te){E.error(te.message||"获取用户分组失败")}finally{l(!1)}};g.useEffect(()=>{v()},[r]);const y=B=>{h(B||null),S.setFieldsValue(B||_u),_(!0)},R=async()=>{if(r)try{const B=await S.validateFields();d(!0);const te={...B,tenant_id:r},J=u?await Fn.update(u.id,te):await Fn.create(te);J.code===0||J.code===200?(E.success(u?"分组已更新":"分组已创建"),_(!1),await v()):E.error(J.message||"保存失败")}catch(B){B!=null&&B.errorFields||E.error(B.message||"保存失败")}finally{d(!1)}},j=async B=>{try{const te=await Fn.delete(B.id,t?r:void 0);te.code===0||te.code===200?(E.success("分组已删除"),await v()):E.error(te.message||"删除失败")}catch(te){E.error(te.message||"删除失败")}},T=()=>{w.setFieldsValue(p||{app_login_ip_whitelist:"",web_login_ip_whitelist:""}),f(!0)},C=async()=>{if(r)try{const B=await w.validateFields();d(!0);const te=await Fn.updateIPWhitelist({tenant_id:r,...B});te.code===0||te.code===200?(E.success("IP 白名单已保存"),f(!1),await v()):E.error(te.message||"保存失败")}catch(B){B!=null&&B.errorFields||E.error(B.message||"保存失败")}finally{d(!1)}},F=g.useMemo(()=>[{title:"分组名称",dataIndex:"name",width:180,render:(B,te)=>a.jsxs(Ie,{children:[a.jsx("span",{children:B}),te.is_default&&a.jsx(Te,{color:"blue",children:"默认"})]})},{title:"用户数",dataIndex:"user_count",width:90},{title:"允许加好友",dataIndex:"allow_add_friend",width:110,render:B=>a.jsx(Te,{color:B?"green":"default",children:B?"允许":"禁止"})},{title:"允许建群",dataIndex:"allow_create_group",width:100,render:B=>a.jsx(Te,{color:B?"green":"default",children:B?"允许":"禁止"})},{title:"直接加好友",dataIndex:"direct_add_friend",width:110,render:B=>B?a.jsx(Te,{color:"cyan",children:"开启"}):"--"},{title:"好友上限",dataIndex:"max_friend_count",width:100,render:B=>B===0?"不限":B},{title:"APP 登录",dataIndex:"allow_app_login",width:100,render:B=>B?"允许":"禁止"},{title:"Web 登录",dataIndex:"allow_web_login",width:100,render:B=>B?"允许":"禁止"},{title:"IP 白名单",dataIndex:"ip_whitelist_enabled",width:110,render:B=>a.jsx(Te,{color:B?"gold":"default",children:B?"已启用":"未启用"})},{title:"状态",dataIndex:"enabled",width:90,render:B=>a.jsx(Te,{color:B?"green":"red",children:B?"启用":"停用"})},{title:"权重",dataIndex:"sort_order",width:80},{title:"操作",key:"actions",fixed:"right",width:145,render:(B,te)=>a.jsxs(Ie,{children:[a.jsx(Y,{type:"text",size:"small",icon:a.jsx(im,{}),onClick:()=>y(te),children:"编辑"}),a.jsx(js,{title:"确认删除该分组?",onConfirm:()=>j(te),disabled:te.is_default,children:a.jsx(Y,{type:"text",size:"small",danger:!0,disabled:te.is_default,children:"删除"})})]})}],[r,t]),P=!!((X=p==null?void 0:p.app_login_ip_whitelist)!=null&&X.trim()),V=!!((_e=p==null?void 0:p.web_login_ip_whitelist)!=null&&_e.trim());return a.jsxs("div",{className:"policy-groups-page",children:[a.jsx("div",{className:"policy-toolbar",children:a.jsxs(Ie,{children:[t&&a.jsx(ot,{value:r,onChange:B=>n(B),style:{width:260}}),a.jsx(Y,{type:"primary",icon:a.jsx(ua,{}),disabled:!r,onClick:()=>y(),children:"添加分组"}),a.jsx(Y,{icon:a.jsx(Ks,{}),disabled:!r,onClick:T,children:"IP 白名单"})]})}),r&&a.jsx(Zs,{className:"policy-ip-status",type:P&&V?"success":"warning",showIcon:!0,message:a.jsxs(Ie,{size:"large",children:[a.jsx("span",{children:"租户渠道 IP 白名单"}),a.jsxs("span",{children:["APP:",P?"已配置":"未配置"]}),a.jsxs("span",{children:["Web:",V?"已配置":"未配置"]})]})}),a.jsx(Ye,{rowKey:"id",loading:o,columns:F,dataSource:s,pagination:!1,scroll:{x:1450}}),a.jsx(Me,{title:u?"编辑分组":"添加分组",open:m,width:620,confirmLoading:c,onOk:R,onCancel:()=>_(!1),destroyOnClose:!0,children:a.jsxs(b,{form:S,layout:"vertical",initialValues:_u,className:"policy-editor-form",children:[a.jsx(b.Item,{label:"分组名称",name:"name",rules:[{required:!0,message:"请输入分组名称"},{max:50}],children:a.jsx(ve,{})}),a.jsxs("div",{className:"policy-switch-grid",children:[a.jsx(b.Item,{label:"默认用户组",name:"is_default",valuePropName:"checked",children:a.jsx(rt,{disabled:!!(u!=null&&u.is_default)})}),a.jsx(b.Item,{label:"启用状态",name:"enabled",valuePropName:"checked",children:a.jsx(rt,{})}),a.jsx(b.Item,{label:"允许加好友",name:"allow_add_friend",valuePropName:"checked",children:a.jsx(rt,{})}),a.jsx(b.Item,{label:"允许建群",name:"allow_create_group",valuePropName:"checked",children:a.jsx(rt,{})}),a.jsx(b.Item,{label:"直接添加好友",name:"direct_add_friend",valuePropName:"checked",children:a.jsx(rt,{})}),a.jsx(b.Item,{label:"允许 APP 登录",name:"allow_app_login",valuePropName:"checked",children:a.jsx(rt,{})}),a.jsx(b.Item,{label:"允许 Web 登录",name:"allow_web_login",valuePropName:"checked",children:a.jsx(rt,{})}),a.jsx(b.Item,{label:"启用 IP 白名单",name:"ip_whitelist_enabled",valuePropName:"checked",children:a.jsx(rt,{})})]}),a.jsxs("div",{className:"policy-number-grid",children:[a.jsx(b.Item,{label:"好友数量上限",name:"max_friend_count",rules:[{required:!0}],children:a.jsx(at,{min:0,max:1e5,precision:0,style:{width:"100%"},addonAfter:"0 为不限"})}),a.jsx(b.Item,{label:"排序权重",name:"sort_order",rules:[{required:!0}],children:a.jsx(at,{min:-1e5,max:1e5,precision:0,style:{width:"100%"}})})]})]})}),a.jsx(Me,{title:"租户渠道 IP 白名单",open:x,width:620,confirmLoading:c,onOk:C,onCancel:()=>f(!1),children:a.jsxs(b,{form:w,layout:"vertical",children:[a.jsx(b.Item,{label:"APP 登录白名单",name:"app_login_ip_whitelist",children:a.jsx(ve.TextArea,{rows:6,placeholder:`每行一个 IPv4、IPv6 或 CIDR +203.0.113.10 +2001:db8::/32`})}),a.jsx(b.Item,{label:"Web 登录白名单",name:"web_login_ip_whitelist",children:a.jsx(ve.TextArea,{rows:6,placeholder:`每行一个 IPv4、IPv6 或 CIDR +198.51.100.0/24`})})]})})]})},Yj={active:{color:"green",label:"正常"},disabled:{color:"red",label:"已封禁"}},qj=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,[r]=b.useForm(),[n,s]=g.useState(!1),[i,o]=g.useState([]),[l,c]=g.useState(1),[d,u]=g.useState(20),[h,m]=g.useState(0),[_,x]=g.useState({}),[f,p]=g.useState(!1),[I,S]=g.useState(""),[w,v]=g.useState([]),[y,R]=g.useState(!1),[j,T]=g.useState(1),[C,F]=g.useState(20),[P,V]=g.useState(0),X=g.useRef(0),_e=g.useRef(0),B=async(W=l,z=d,D=_)=>{const q=++X.current;if(t&&!D.tenant_id){o([]),m(0),s(!1);return}s(!0);try{const K=await rd.list({...D,page:W,page_size:z}),Z=K.code===0||K.code===200;if(q!==X.current)return;if(!Z||!K.data){E.error(K.message||"获取 IP 统计失败");return}o(K.data.items),m(K.data.total),c(K.data.page),u(K.data.size)}catch(K){q===X.current&&E.error(K.message||"获取 IP 统计失败")}finally{q===X.current&&s(!1)}};g.useEffect(()=>{e&&(t||B(1,d,{}))},[e,t]);const te=W=>{var D;const z={tenant_id:W.tenant_id?Number(W.tenant_id):void 0,ip:((D=W.ip)==null?void 0:D.trim())||void 0};p(!1),x(z),B(1,d,z)},J=()=>{r.resetFields(),p(!1),x({}),t?(o([]),m(0)):B(1,d,{})},G=async(W,z=1,D=C)=>{const q=++_e.current;R(!0);try{const K=await rd.users({tenant_id:_.tenant_id,ip:W,page:z,page_size:D}),Z=K.code===0||K.code===200;if(q!==_e.current)return;if(!Z||!K.data){E.error(K.message||"获取关联用户失败");return}v(K.data.items),V(K.data.total),T(K.data.page),F(K.data.size)}catch(K){q===_e.current&&E.error(K.message||"获取关联用户失败")}finally{q===_e.current&&R(!1)}},se=W=>{S(W.ip),p(!0),G(W.ip,1,C)},Q=W=>{const z=_.tenant_id;z&&Me.confirm({title:"封禁该 IP 关联用户",content:`确认封禁当前租户内使用 IP「${W.ip}」注册或登录的全部账号?命中的账号会立即撤销登录会话并强制下线。`,okText:"确认封禁并下线",okButtonProps:{danger:!0},cancelText:"取消",onOk:async()=>{var D;try{const q=await Lt.banByIP(W.ip,z);if(!(q.code===0||q.code===200)||!q.data){E.error(q.message||"按 IP 封禁失败");return}E.success(`命中 ${q.data.matched_count} 人,成功 ${q.data.success_count} 人`),B(l,d,_),f&&I===W.ip&&G(W.ip,j,C)}catch(q){if((D=q==null?void 0:q.message)!=null&&D.includes("timeout")){E.warning("请求超时,后台可能仍在处理,请稍后刷新确认状态");return}E.error(q.message||"按 IP 封禁失败")}}})},de=[{title:"IP 地址",dataIndex:"ip",width:260,render:W=>a.jsx("span",{style:{wordBreak:"break-all"},children:W})},{title:"归属地",dataIndex:"ip_location",width:140,render:W=>W||"未知"},{title:"关联用户",dataIndex:"user_count",width:120,render:W=>a.jsxs(Ie,{size:6,children:[a.jsx("span",{children:W}),W>1&&a.jsx(Te,{color:"red",children:"多账号"})]})},{title:"注册用户",dataIndex:"register_user_count",width:110},{title:"登录用户",dataIndex:"login_user_count",width:110},{title:"正常",dataIndex:"active_user_count",width:90},{title:"已封禁",dataIndex:"disabled_user_count",width:90},{title:"最近活动",dataIndex:"last_seen_at",width:180,render:W=>Ke(W)},{title:"操作",key:"actions",fixed:"right",width:t?190:100,render:(W,z)=>a.jsxs(Ie,{children:[a.jsx(Y,{size:"small",icon:a.jsx(xl,{}),onClick:()=>se(z),children:"用户列表"}),t&&a.jsx(Y,{size:"small",danger:!0,icon:a.jsx(ju,{}),onClick:()=>Q(z),children:"封禁 IP"})]})}],k=[...t?[{title:"用户 ID",dataIndex:"user_id",width:100}]:[],{title:"用户编号",dataIndex:"user_no",width:110},{title:"用户名",dataIndex:"username",width:150},{title:"昵称",dataIndex:"nickname",width:150},{title:"来源",key:"source",width:150,render:(W,z)=>a.jsxs(Ie,{size:4,children:[z.matched_register&&a.jsx(Te,{color:"blue",children:"注册"}),z.matched_login&&a.jsx(Te,{color:"green",children:"登录"})]})},{title:"状态",dataIndex:"status",width:100,render:W=>{const z=Yj[W];return a.jsx(Te,{color:z.color,children:z.label})}},{title:"最近活动",dataIndex:"last_seen_at",width:180,render:W=>Ke(W)}];return a.jsxs("div",{children:[a.jsx(be,{style:{marginBottom:16},children:a.jsxs(b,{form:r,layout:"inline",onFinish:te,children:[t&&a.jsx(b.Item,{label:"租户",name:"tenant_id",rules:[{required:!0,message:"请选择租户"}],children:a.jsx(ot,{style:{width:220}})}),a.jsx(b.Item,{label:"IP",name:"ip",children:a.jsx(ve,{placeholder:"IPv4 或 IPv6",allowClear:!0,style:{width:220}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:J,children:"重置"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:"ip",columns:de,dataSource:i,loading:n,scroll:{x:1390},pagination:{current:l,pageSize:d,total:h,showSizeChanger:!0,showTotal:W=>`共 ${W} 个 IP`,onChange:(W,z)=>{c(W),u(z),B(W,z)}}})}),a.jsx(Me,{title:`IP ${I} 的关联用户`,open:f,onCancel:()=>p(!1),footer:null,width:980,destroyOnClose:!0,children:a.jsx(Ye,{rowKey:"user_id",columns:k,dataSource:w,loading:y,scroll:{x:900},pagination:{current:j,pageSize:C,total:P,showSizeChanger:!0,onChange:(W,z)=>G(I,W,z)}})})]})},{RangePicker:Xj}=jr,Oh=[{label:"修改昵称",value:"change_nickname"},{label:"修改头像",value:"change_avatar"},{label:"修改性别",value:"change_gender"},{label:"修改签名",value:"change_signature"},{label:"修改生日",value:"change_birthday"},{label:"修改密码",value:"change_password"},{label:"主动注销",value:"deactivate_account"},{label:"管理员修改用户名",value:"update_username"},{label:"管理员更新状态",value:"update_status"},{label:"管理员重置密码",value:"reset_password"},{label:"管理员更新安全权限",value:"update_security_perm"},{label:"管理员强制下线",value:"kick_user"},{label:"管理员强制注销",value:"force_deactivate"},{label:"管理员调整分组",value:"assign_policy_group"},{label:"管理员批量封禁",value:"batch_ban_users"},{label:"管理员按 IP 封禁",value:"ban_users_by_ip"}],Rh=Object.fromEntries(Oh.map(e=>[e.value,e.label])),cn=e=>e==null||e===""?"空":String(e),Jj=e=>{var t,r,n,s,i,o,l,c,d;if(e.details)return e.details;switch(e.action){case"change_nickname":return`${cn((t=e.before_data)==null?void 0:t.nickname)} → ${cn((r=e.after_data)==null?void 0:r.nickname)}`;case"change_avatar":return"已修改头像";case"change_gender":return`${cn((n=e.before_data)==null?void 0:n.gender)} → ${cn((s=e.after_data)==null?void 0:s.gender)}`;case"change_signature":return`${cn((i=e.before_data)==null?void 0:i.signature)} → ${cn((o=e.after_data)==null?void 0:o.signature)}`;case"change_birthday":return`${cn((l=e.before_data)==null?void 0:l.birthday)} → ${cn((c=e.after_data)==null?void 0:c.birthday)}`;case"change_password":return"密码已修改";case"deactivate_account":return"账号已主动注销";default:return(d=e.after_data)!=null&&d.details?cn(e.after_data.details):Rh[e.action]||e.action}},Zj=()=>{const{admin:e}=pt(),t=!!e&&!e.tenant_id,[r]=b.useForm(),[n,s]=g.useState(!1),[i,o]=g.useState([]),[l,c]=g.useState(1),[d,u]=g.useState(20),[h,m]=g.useState(0),[_,x]=g.useState({}),f=g.useRef(0),p=async(v=l,y=d,R=_)=>{const j=++f.current;if(t&&!R.tenant_id){o([]),m(0),s(!1);return}s(!0);try{const T=await v_.list({...R,page:v,page_size:y});if(j!==f.current)return;if(!(T.code===0||T.code===200)||!T.data){E.error(T.message||"获取用户账号审计日志失败");return}o(T.data.items),m(T.data.total),c(T.data.page),u(T.data.size)}catch(T){j===f.current&&E.error(T.message||"获取用户账号审计日志失败")}finally{j===f.current&&s(!1)}};g.useEffect(()=>{e&&(t||p(1,d,{}))},[e,t]);const I=v=>{var j,T,C,F;const y=v.time_range,R={tenant_id:v.tenant_id?Number(v.tenant_id):void 0,user_id:t&&v.user_id?Number(v.user_id):void 0,user_no:!t&&v.user_no?Number(v.user_no):void 0,username:((j=v.username)==null?void 0:j.trim())||void 0,action:v.action||void 0,actor_type:v.actor_type||void 0,ip:((T=v.ip)==null?void 0:T.trim())||void 0,start_time:(C=y==null?void 0:y[0])==null?void 0:C.toISOString(),end_time:(F=y==null?void 0:y[1])==null?void 0:F.toISOString()};x(R),p(1,d,R)},S=()=>{r.resetFields(),x({}),t?(f.current+=1,o([]),m(0),s(!1)):p(1,d,{})},w=[{title:"操作时间",dataIndex:"created_at",width:180,render:v=>Ke(v)},{title:"目标用户",key:"target_user",width:190,render:(v,y)=>a.jsxs("div",{children:[a.jsx("div",{children:y.target_username||`用户 ${y.target_user_id}`}),a.jsx("div",{style:{color:"#8c8c8c",fontSize:12},children:t?`ID:${y.target_user_id}`:`编号:${y.target_user_no||"-"}`})]})},{title:"操作者",key:"actor",width:170,render:(v,y)=>a.jsxs(Ie,{size:6,children:[a.jsx(Te,{color:y.actor_type==="admin"?"blue":"green",children:y.actor_type==="admin"?"管理员":"用户本人"}),a.jsx("span",{children:y.actor_username||"-"})]})},{title:"操作类型",dataIndex:"action",width:160,render:v=>Rh[v]||v},{title:"变更摘要",key:"summary",width:260,ellipsis:!0,render:(v,y)=>{const R=Jj(y);return a.jsx(Xt,{title:R,children:R})}},{title:"IP / 归属地",key:"ip",width:210,render:(v,y)=>y.ip?a.jsxs("div",{style:{lineHeight:1.45},children:[a.jsx("div",{style:{fontFamily:"Consolas, monospace"},children:y.ip}),a.jsxs("div",{style:{color:"#8c8c8c",fontSize:12},children:[a.jsx(om,{style:{marginRight:4}}),y.ip_location||"未知归属地"]})]}):"-"},{title:"设备/客户端",key:"client",width:220,ellipsis:!0,render:(v,y)=>{const R=y.device_id||y.user_agent||"-";return a.jsx(Xt,{title:y.user_agent||R,children:R})}}];return a.jsxs("div",{children:[a.jsx(be,{style:{marginBottom:16},children:a.jsxs(b,{form:r,layout:"inline",onFinish:I,children:[t&&a.jsx(b.Item,{label:"租户",name:"tenant_id",rules:[{required:!0,message:"请选择租户"}],children:a.jsx(ot,{style:{width:220}})}),t?a.jsx(b.Item,{label:"用户ID",name:"user_id",children:a.jsx(at,{min:1,precision:0,style:{width:130}})}):a.jsx(b.Item,{label:"用户编号",name:"user_no",children:a.jsx(at,{min:1,precision:0,style:{width:130}})}),a.jsx(b.Item,{label:"用户名",name:"username",children:a.jsx(ve,{allowClear:!0,style:{width:150}})}),a.jsx(b.Item,{label:"操作者",name:"actor_type",children:a.jsx($e,{allowClear:!0,style:{width:130},options:[{label:"用户本人",value:"user"},{label:"管理员",value:"admin"}]})}),a.jsx(b.Item,{label:"操作",name:"action",children:a.jsx($e,{allowClear:!0,showSearch:!0,optionFilterProp:"label",style:{width:180},options:Oh})}),a.jsx(b.Item,{label:"IP",name:"ip",children:a.jsx(ve,{allowClear:!0,style:{width:150}})}),a.jsx(b.Item,{label:"时间",name:"time_range",children:a.jsx(Xj,{showTime:!0,style:{width:340}})}),a.jsx(b.Item,{children:a.jsxs(Ie,{children:[a.jsx(Y,{type:"primary",htmlType:"submit",icon:a.jsx(Ar,{}),children:"查询"}),a.jsx(Y,{onClick:S,children:"重置"})]})})]})}),a.jsx(be,{children:a.jsx(Ye,{rowKey:v=>`${v.source}:${v.id}`,columns:w,dataSource:i,loading:n,scroll:{x:1330},pagination:{current:l,pageSize:d,total:h,showSizeChanger:!0,showTotal:v=>`共 ${v} 条`,onChange:(v,y)=>p(v,y)}})})]})},Qj=({children:e})=>{const{isAuthenticated:t}=pt();return t?a.jsx(a.Fragment,{children:e}):a.jsx(la,{to:"/login",replace:!0})},tt=({permission:e,platformOnly:t,children:r})=>{const{permissions:n,admin:s}=pt();return n?t&&(s!=null&&s.tenant_id)?a.jsx(la,{to:"/dashboard",replace:!0}):e&&!(typeof e=="function"?e(n):n[e])?a.jsx(la,{to:"/dashboard",replace:!0}):a.jsx(a.Fragment,{children:r}):null},Vs=e=>e.view_private_chat||e.view_group_chat,e4=e=>e.user_manage||e.tenant_manage;function t4(){return a.jsxs(Bh,{children:[a.jsx(Ge,{path:"/login",element:a.jsx(F_,{})}),a.jsxs(Ge,{path:"/",element:a.jsx(Qj,{children:a.jsx(C_,{})}),children:[a.jsx(Ge,{index:!0,element:a.jsx(la,{to:"/dashboard",replace:!0})}),a.jsx(Ge,{path:"dashboard",element:a.jsx(tt,{children:a.jsx(O_,{})})}),a.jsx(Ge,{path:"tenants",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(KE,{})})}),a.jsx(Ge,{path:"admins",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(YE,{})})}),a.jsx(Ge,{path:"tenant-admins",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(yj,{})})}),a.jsx(Ge,{path:"roles",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(qE,{})})}),a.jsx(Ge,{path:"users",element:a.jsx(tt,{permission:"user_manage",children:a.jsx(jE,{})})}),a.jsx(Ge,{path:"user-policy-groups",element:a.jsx(tt,{permission:"user_manage",children:a.jsx(Gj,{})})}),a.jsx(Ge,{path:"groups",element:a.jsx(tt,{permission:"group_manage",children:a.jsx(kE,{})})}),a.jsx(Ge,{path:"group-members",element:a.jsx(tt,{permission:"group_manage",children:a.jsx(rj,{})})}),a.jsx(Ge,{path:"quick-create-group",element:a.jsx(tt,{platformOnly:!0,children:a.jsx(Kj,{})})}),a.jsx(Ge,{path:"private-messages",element:a.jsx(tt,{permission:"view_private_chat",children:a.jsx(DE,{})})}),a.jsx(Ge,{path:"group-messages",element:a.jsx(tt,{permission:"view_group_chat",children:a.jsx(PE,{})})}),a.jsx(Ge,{path:"invite-codes",element:a.jsx(tt,{permission:e4,children:a.jsx(ZE,{})})}),a.jsx(Ge,{path:"announcements",element:a.jsx(tt,{permission:Vs,children:a.jsx(QE,{})})}),a.jsx(Ge,{path:"sensitive-words",element:a.jsx(tt,{permission:Vs,children:a.jsx(ej,{})})}),a.jsx(Ge,{path:"audit-logs",element:a.jsx(tt,{permission:"view_audit_log",children:a.jsx(HE,{})})}),a.jsx(Ge,{path:"user-account-audit",element:a.jsx(tt,{permission:"view_audit_log",children:a.jsx(Zj,{})})}),a.jsx(Ge,{path:"nodes",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(lj,{})})}),a.jsx(Ge,{path:"group-events",element:a.jsx(tt,{permission:"view_audit_log",children:a.jsx(fj,{})})}),a.jsx(Ge,{path:"avatar-pool",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(mj,{})})}),a.jsx(Ge,{path:"system-conversations",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(xj,{})})}),a.jsx(Ge,{path:"system-notices",element:a.jsx(tt,{platformOnly:!0,children:a.jsx(pj,{})})}),a.jsx(Ge,{path:"reports",element:a.jsx(tt,{permission:Vs,platformOnly:!0,children:a.jsx(vj,{})})}),a.jsx(Ge,{path:"admin-login-history",element:a.jsx(tt,{children:a.jsx(wj,{})})}),a.jsx(Ge,{path:"user-login-history",element:a.jsx(tt,{children:a.jsx(Sj,{})})}),a.jsx(Ge,{path:"ip-user-stats",element:a.jsx(tt,{children:a.jsx(qj,{})})}),a.jsx(Ge,{path:"login-streak-ranking",element:a.jsx(tt,{platformOnly:!0,children:a.jsx(bj,{})})}),a.jsx(Ge,{path:"private-msg-ranking",element:a.jsx(tt,{platformOnly:!0,children:a.jsx(Ej,{})})}),a.jsx(Ge,{path:"user-password-query",element:a.jsx(tt,{platformOnly:!0,children:a.jsx(jj,{})})}),a.jsx(Ge,{path:"keyword-alerts",element:a.jsx(tt,{platformOnly:!0,children:a.jsx(Aj,{})})}),a.jsx(Ge,{path:"alert-bot-config",element:a.jsx(tt,{platformOnly:!0,children:a.jsx(kj,{})})}),a.jsx(Ge,{path:"tenant-settings",element:a.jsx(tt,{children:a.jsx(Rj,{})})}),a.jsx(Ge,{path:"file-records",element:a.jsx(tt,{permission:Vs,children:a.jsx(Dj,{})})}),a.jsx(Ge,{path:"security-settings",element:a.jsx(tt,{children:a.jsx(Pj,{})})}),a.jsx(Ge,{path:"login-failure-logs",element:a.jsx(tt,{children:a.jsx(Mj,{})})}),a.jsx(Ge,{path:"domain-encrypt",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(Uj,{})})}),a.jsx(Ge,{path:"system-status",element:a.jsx(tt,{permission:"tenant_manage",children:a.jsx(Oj,{})})}),a.jsx(Ge,{path:"feedbacks",element:a.jsx(tt,{permission:Vs,children:a.jsx(Wj,{})})})]}),a.jsx(Ge,{path:"*",element:a.jsx(la,{to:"/",replace:!0})})]})}const vu="xinxunda-admin:chunk-reload",r4=e=>{const t=e instanceof Error?e.message:String(e);return/ChunkLoadError|Loading chunk|Failed to fetch dynamically imported module|Importing a module script failed|dynamically imported module|404/i.test(t)},n4=()=>typeof window>"u"||window.sessionStorage.getItem(vu)==="1"?!1:(window.sessionStorage.setItem(vu,"1"),window.location.reload(),!0),s4=e=>{r4(e.payload)&&(e.preventDefault(),n4())};window.addEventListener("vite:preloadError",s4);Jo.createRoot(document.getElementById("root")).render(a.jsx(Gt.StrictMode,{children:a.jsx(Uh,{children:a.jsx(lm,{locale:Dm,children:a.jsx(t4,{})})})})); diff --git a/docs/admin-adfahi-research/screenshots-queried/00-menu-overview.png b/docs/admin-adfahi-research/screenshots-queried/00-menu-overview.png new file mode 100644 index 0000000..5299a52 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/00-menu-overview.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/01-dashboard.png b/docs/admin-adfahi-research/screenshots-queried/01-dashboard.png new file mode 100644 index 0000000..73c5cfd Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/01-dashboard.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/02-user-management.png b/docs/admin-adfahi-research/screenshots-queried/02-user-management.png new file mode 100644 index 0000000..1a1d406 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/02-user-management.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/03-user-groups.png b/docs/admin-adfahi-research/screenshots-queried/03-user-groups.png new file mode 100644 index 0000000..c426aef Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/03-user-groups.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/04-group-management.png b/docs/admin-adfahi-research/screenshots-queried/04-group-management.png new file mode 100644 index 0000000..302193d Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/04-group-management.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/05-group-members.png b/docs/admin-adfahi-research/screenshots-queried/05-group-members.png new file mode 100644 index 0000000..cfd14fe Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/05-group-members.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/06-invite-codes.png b/docs/admin-adfahi-research/screenshots-queried/06-invite-codes.png new file mode 100644 index 0000000..17d69fb Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/06-invite-codes.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/07-direct-messages.png b/docs/admin-adfahi-research/screenshots-queried/07-direct-messages.png new file mode 100644 index 0000000..b82e77d Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/07-direct-messages.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/08-group-messages.png b/docs/admin-adfahi-research/screenshots-queried/08-group-messages.png new file mode 100644 index 0000000..38fb2b2 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/08-group-messages.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/09-sensitive-words.png b/docs/admin-adfahi-research/screenshots-queried/09-sensitive-words.png new file mode 100644 index 0000000..f2d1cb6 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/09-sensitive-words.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/10-files.png b/docs/admin-adfahi-research/screenshots-queried/10-files.png new file mode 100644 index 0000000..f8681c5 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/10-files.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/11-feedback.png b/docs/admin-adfahi-research/screenshots-queried/11-feedback.png new file mode 100644 index 0000000..e627429 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/11-feedback.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/12-operation-logs.png b/docs/admin-adfahi-research/screenshots-queried/12-operation-logs.png new file mode 100644 index 0000000..f96bb01 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/12-operation-logs.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/13-user-account-audit.png b/docs/admin-adfahi-research/screenshots-queried/13-user-account-audit.png new file mode 100644 index 0000000..aa2fee2 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/13-user-account-audit.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/14-group-event-audit.png b/docs/admin-adfahi-research/screenshots-queried/14-group-event-audit.png new file mode 100644 index 0000000..5f084de Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/14-group-event-audit.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/15-admin-login.png b/docs/admin-adfahi-research/screenshots-queried/15-admin-login.png new file mode 100644 index 0000000..2674b05 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/15-admin-login.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/16-user-login.png b/docs/admin-adfahi-research/screenshots-queried/16-user-login.png new file mode 100644 index 0000000..8cfb5d4 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/16-user-login.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/17-ip-account-stats.png b/docs/admin-adfahi-research/screenshots-queried/17-ip-account-stats.png new file mode 100644 index 0000000..6caac75 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/17-ip-account-stats.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/18-login-failures.png b/docs/admin-adfahi-research/screenshots-queried/18-login-failures.png new file mode 100644 index 0000000..4a16770 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/18-login-failures.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/19-private-domain-settings.png b/docs/admin-adfahi-research/screenshots-queried/19-private-domain-settings.png new file mode 100644 index 0000000..a5f5272 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/19-private-domain-settings.png differ diff --git a/docs/admin-adfahi-research/screenshots-queried/20-security-policy.png b/docs/admin-adfahi-research/screenshots-queried/20-security-policy.png new file mode 100644 index 0000000..782ab97 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots-queried/20-security-policy.png differ diff --git a/docs/admin-adfahi-research/screenshots/00-menu-overview.png b/docs/admin-adfahi-research/screenshots/00-menu-overview.png new file mode 100644 index 0000000..5299a52 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/00-menu-overview.png differ diff --git a/docs/admin-adfahi-research/screenshots/01-dashboard.png b/docs/admin-adfahi-research/screenshots/01-dashboard.png new file mode 100644 index 0000000..cb9baf9 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/01-dashboard.png differ diff --git a/docs/admin-adfahi-research/screenshots/02-user-management.png b/docs/admin-adfahi-research/screenshots/02-user-management.png new file mode 100644 index 0000000..77ed862 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/02-user-management.png differ diff --git a/docs/admin-adfahi-research/screenshots/03-user-groups.png b/docs/admin-adfahi-research/screenshots/03-user-groups.png new file mode 100644 index 0000000..746df5f Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/03-user-groups.png differ diff --git a/docs/admin-adfahi-research/screenshots/04-group-management.png b/docs/admin-adfahi-research/screenshots/04-group-management.png new file mode 100644 index 0000000..39f5997 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/04-group-management.png differ diff --git a/docs/admin-adfahi-research/screenshots/05-group-members.png b/docs/admin-adfahi-research/screenshots/05-group-members.png new file mode 100644 index 0000000..79bbc9f Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/05-group-members.png differ diff --git a/docs/admin-adfahi-research/screenshots/06-invite-codes.png b/docs/admin-adfahi-research/screenshots/06-invite-codes.png new file mode 100644 index 0000000..5326ca0 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/06-invite-codes.png differ diff --git a/docs/admin-adfahi-research/screenshots/07-direct-messages.png b/docs/admin-adfahi-research/screenshots/07-direct-messages.png new file mode 100644 index 0000000..73e2966 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/07-direct-messages.png differ diff --git a/docs/admin-adfahi-research/screenshots/08-group-messages.png b/docs/admin-adfahi-research/screenshots/08-group-messages.png new file mode 100644 index 0000000..40f37fb Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/08-group-messages.png differ diff --git a/docs/admin-adfahi-research/screenshots/09-sensitive-words.png b/docs/admin-adfahi-research/screenshots/09-sensitive-words.png new file mode 100644 index 0000000..14a3f0f Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/09-sensitive-words.png differ diff --git a/docs/admin-adfahi-research/screenshots/10-files.png b/docs/admin-adfahi-research/screenshots/10-files.png new file mode 100644 index 0000000..e0410d9 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/10-files.png differ diff --git a/docs/admin-adfahi-research/screenshots/11-feedback.png b/docs/admin-adfahi-research/screenshots/11-feedback.png new file mode 100644 index 0000000..af91a13 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/11-feedback.png differ diff --git a/docs/admin-adfahi-research/screenshots/12-operation-logs.png b/docs/admin-adfahi-research/screenshots/12-operation-logs.png new file mode 100644 index 0000000..a0c0e8c Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/12-operation-logs.png differ diff --git a/docs/admin-adfahi-research/screenshots/13-user-account-audit.png b/docs/admin-adfahi-research/screenshots/13-user-account-audit.png new file mode 100644 index 0000000..816c9a8 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/13-user-account-audit.png differ diff --git a/docs/admin-adfahi-research/screenshots/14-group-event-audit.png b/docs/admin-adfahi-research/screenshots/14-group-event-audit.png new file mode 100644 index 0000000..03be1cd Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/14-group-event-audit.png differ diff --git a/docs/admin-adfahi-research/screenshots/15-admin-login.png b/docs/admin-adfahi-research/screenshots/15-admin-login.png new file mode 100644 index 0000000..703bee6 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/15-admin-login.png differ diff --git a/docs/admin-adfahi-research/screenshots/16-user-login.png b/docs/admin-adfahi-research/screenshots/16-user-login.png new file mode 100644 index 0000000..2d25c57 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/16-user-login.png differ diff --git a/docs/admin-adfahi-research/screenshots/17-ip-account-stats.png b/docs/admin-adfahi-research/screenshots/17-ip-account-stats.png new file mode 100644 index 0000000..3a1e44e Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/17-ip-account-stats.png differ diff --git a/docs/admin-adfahi-research/screenshots/18-login-failures.png b/docs/admin-adfahi-research/screenshots/18-login-failures.png new file mode 100644 index 0000000..80b4788 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/18-login-failures.png differ diff --git a/docs/admin-adfahi-research/screenshots/19-private-domain-settings.png b/docs/admin-adfahi-research/screenshots/19-private-domain-settings.png new file mode 100644 index 0000000..73af3bf Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/19-private-domain-settings.png differ diff --git a/docs/admin-adfahi-research/screenshots/20-security-policy.png b/docs/admin-adfahi-research/screenshots/20-security-policy.png new file mode 100644 index 0000000..4d975a5 Binary files /dev/null and b/docs/admin-adfahi-research/screenshots/20-security-policy.png differ diff --git a/docs/admin-adfahi-research/sql/Admin-Server改造.sql b/docs/admin-adfahi-research/sql/Admin-Server改造.sql new file mode 100644 index 0000000..3d30b4b --- /dev/null +++ b/docs/admin-adfahi-research/sql/Admin-Server改造.sql @@ -0,0 +1,350 @@ +-- Admin Server schema changes for the GedaDa management-console adaptation. +-- Target: MySQL 8.0+, control database `hotgo`. +-- Scope: control-plane configuration only. Do not create tenant business data here. +-- Review before execution. DDL statements auto-commit in MySQL. + +SET NAMES utf8mb4; +USE `hotgo`; + +-- ============================================================ +-- 1. User policy groups. +-- status: 1=enabled, 2=disabled. +-- policy_json action values: allow, deny, challenge, audit_only. +-- Friend entry values: search, profile_card, group_member, qrcode, +-- invite_auto_add, recommendation. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `im_user_policy_groups` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '0 means global default', + `group_code` varchar(64) NOT NULL COMMENT 'Stable policy group code', + `group_name` varchar(128) NOT NULL COMMENT 'Policy group name', + `priority` int NOT NULL DEFAULT 0 COMMENT 'Higher value has higher priority', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled', + `policy_json` json NOT NULL COMMENT 'Login, friend, message, group and file policies', + `ip_whitelist` json DEFAULT NULL COMMENT 'IPv4/IPv6 or CIDR list', + `ip_blacklist` json DEFAULT NULL COMMENT 'IPv4/IPv6 or CIDR list', + `version` int unsigned NOT NULL DEFAULT 1 COMMENT 'Optimistic-lock/config version', + `remark` varchar(500) NOT NULL DEFAULT '', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` bigint NOT NULL DEFAULT 0, + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `updated_by` bigint NOT NULL DEFAULT 0, + `deleted_at` datetime(3) DEFAULT NULL, + `deleted_by` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_user_policy_group_code` (`tenant_id`, `group_code`), + KEY `idx_im_user_policy_group_list` (`tenant_id`, `status`, `priority`, `id`), + KEY `idx_im_user_policy_group_deleted` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM user policy groups'; + +CREATE TABLE IF NOT EXISTS `im_user_policy_group_members` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT 0, + `group_id` bigint NOT NULL, + `user_id` bigint NOT NULL COMMENT 'User ID in the tenant database', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled', + `valid_from` datetime(3) DEFAULT NULL, + `valid_until` datetime(3) DEFAULT NULL, + `remark` varchar(500) NOT NULL DEFAULT '', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` bigint NOT NULL DEFAULT 0, + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `updated_by` bigint NOT NULL DEFAULT 0, + `deleted_at` datetime(3) DEFAULT NULL, + `deleted_by` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_user_policy_group_member` (`group_id`, `user_id`), + KEY `idx_im_user_policy_member_user` (`tenant_id`, `user_id`, `status`), + KEY `idx_im_user_policy_member_validity` (`status`, `valid_from`, `valid_until`), + KEY `idx_im_user_policy_member_deleted` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM user-to-policy-group assignments'; + +-- ============================================================ +-- 2. Sensitive word dictionary. +-- match_type: exact, contains, regex, pinyin. +-- action: replace, block, audit_only, manual_review. +-- risk_level: low, medium, high. +-- scene_json values include private_message, group_message, user_profile, +-- group_profile, feedback, report. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `im_sensitive_word_groups` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '0 means global dictionary', + `group_code` varchar(64) NOT NULL, + `group_name` varchar(128) NOT NULL, + `priority` int NOT NULL DEFAULT 0, + `status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled', + `remark` varchar(500) NOT NULL DEFAULT '', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` bigint NOT NULL DEFAULT 0, + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `updated_by` bigint NOT NULL DEFAULT 0, + `deleted_at` datetime(3) DEFAULT NULL, + `deleted_by` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_sensitive_word_group_code` (`tenant_id`, `group_code`), + KEY `idx_im_sensitive_word_group_list` (`tenant_id`, `status`, `priority`, `id`), + KEY `idx_im_sensitive_word_group_deleted` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM sensitive word groups'; + +CREATE TABLE IF NOT EXISTS `im_sensitive_words` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '0 means global dictionary', + `group_id` bigint NOT NULL, + `word` varchar(255) NOT NULL, + `word_hash` char(64) NOT NULL COMMENT 'SHA-256 of normalized word', + `match_type` varchar(24) NOT NULL DEFAULT 'contains', + `scene_json` json NOT NULL COMMENT 'Enabled scenes', + `action` varchar(24) NOT NULL DEFAULT 'block', + `replacement` varchar(255) NOT NULL DEFAULT '', + `risk_level` varchar(16) NOT NULL DEFAULT 'medium', + `priority` int NOT NULL DEFAULT 0, + `status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled', + `remark` varchar(500) NOT NULL DEFAULT '', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` bigint NOT NULL DEFAULT 0, + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `updated_by` bigint NOT NULL DEFAULT 0, + `deleted_at` datetime(3) DEFAULT NULL, + `deleted_by` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_sensitive_word_normalized` (`tenant_id`, `group_id`, `word_hash`), + KEY `idx_im_sensitive_word_list` (`tenant_id`, `status`, `risk_level`, `id`), + KEY `idx_im_sensitive_word_group` (`group_id`, `status`, `priority`), + KEY `idx_im_sensitive_word_deleted` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM sensitive words'; + +-- ============================================================ +-- 3. Versioned security policy. +-- publish_status: draft, published, archived, failed. +-- The latest published row is the source of truth. The summary switches in +-- im_tenant_config are updated in the same application transaction on publish. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `im_security_policy` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '0 means global fallback policy', + `policy_version` int unsigned NOT NULL, + `policy_name` varchar(128) NOT NULL DEFAULT '', + `publish_status` varchar(16) NOT NULL DEFAULT 'draft', + `password_policy` json DEFAULT NULL, + `login_rate_limit` json DEFAULT NULL, + `sensitive_word_policy` json DEFAULT NULL, + `file_review_policy` json DEFAULT NULL, + `realname_policy` json DEFAULT NULL, + `risk_challenge_policy` json DEFAULT NULL, + `global_friend_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Backend master switch for every add-friend entry', + `friend_disabled_policy` json DEFAULT NULL COMMENT 'Profile masking, QR and group-member restrictions', + `message_curfew_enabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Backend master switch for message curfew', + `remark` varchar(500) NOT NULL DEFAULT '', + `published_at` datetime(3) DEFAULT NULL, + `published_by` bigint DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` bigint NOT NULL DEFAULT 0, + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `updated_by` bigint NOT NULL DEFAULT 0, + `deleted_at` datetime(3) DEFAULT NULL, + `deleted_by` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_security_policy_version` (`tenant_id`, `policy_version`), + KEY `idx_im_security_policy_publish` (`tenant_id`, `publish_status`, `published_at`), + KEY `idx_im_security_policy_deleted` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Versioned IM security policies'; + +CREATE TABLE IF NOT EXISTS `im_message_curfew_rules` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT 0, + `policy_id` bigint NOT NULL, + `rule_code` varchar(64) NOT NULL, + `rule_name` varchar(128) NOT NULL, + `date_type` varchar(16) NOT NULL DEFAULT 'daily' COMMENT 'daily, weekday, weekend, custom', + `custom_dates` json DEFAULT NULL COMMENT 'YYYY-MM-DD values for custom date type', + `weekdays` json DEFAULT NULL COMMENT 'ISO weekdays 1 through 7', + `timezone` varchar(64) NOT NULL DEFAULT 'Asia/Shanghai', + `start_time` time NOT NULL, + `end_time` time NOT NULL COMMENT 'Less than start_time means crossing midnight', + `scope` varchar(24) NOT NULL DEFAULT 'private_message' COMMENT 'private_message or group_message', + `whitelist_user_ids` json DEFAULT NULL, + `whitelist_group_ids` json DEFAULT NULL, + `message` varchar(255) NOT NULL DEFAULT '当前时段暂不能发送消息', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled', + `sort` int NOT NULL DEFAULT 0, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` bigint NOT NULL DEFAULT 0, + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `updated_by` bigint NOT NULL DEFAULT 0, + `deleted_at` datetime(3) DEFAULT NULL, + `deleted_by` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_message_curfew_rule` (`policy_id`, `rule_code`), + KEY `idx_im_message_curfew_policy` (`tenant_id`, `policy_id`, `status`, `sort`), + KEY `idx_im_message_curfew_deleted` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Message curfew rules'; + +CREATE TABLE IF NOT EXISTS `im_security_ip_rules` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT 0, + `rule_type` varchar(16) NOT NULL COMMENT 'whitelist or blacklist', + `ip_cidr` varchar(64) NOT NULL COMMENT 'IPv4, IPv6 or CIDR', + `action` varchar(24) NOT NULL DEFAULT 'block' COMMENT 'allow, challenge, block, audit_only', + `scene_json` json NOT NULL COMMENT 'login, register, message, upload and other scenes', + `risk_level` varchar(16) NOT NULL DEFAULT 'high', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled', + `expires_at` datetime(3) DEFAULT NULL, + `remark` varchar(500) NOT NULL DEFAULT '', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` bigint NOT NULL DEFAULT 0, + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `updated_by` bigint NOT NULL DEFAULT 0, + `deleted_at` datetime(3) DEFAULT NULL, + `deleted_by` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_security_ip_rule` (`tenant_id`, `rule_type`, `ip_cidr`), + KEY `idx_im_security_ip_rule_list` (`tenant_id`, `status`, `rule_type`, `id`), + KEY `idx_im_security_ip_rule_expiry` (`status`, `expires_at`), + KEY `idx_im_security_ip_rule_deleted` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM IP whitelist and blacklist rules'; + +-- ============================================================ +-- 4. Lightweight published-policy summary in im_tenant_config. +-- These columns are read optimizations, not the policy source of truth. +-- The conditional ALTER blocks are safe to rerun on MySQL 8.0. +-- ============================================================ + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config' + AND COLUMN_NAME = 'default_policy_group_id') = 0, + 'ALTER TABLE `im_tenant_config` ADD COLUMN `default_policy_group_id` BIGINT DEFAULT NULL COMMENT ''Default user policy group'' AFTER `max_group_members`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config' + AND COLUMN_NAME = 'security_policy_version') = 0, + 'ALTER TABLE `im_tenant_config` ADD COLUMN `security_policy_version` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''Published security policy version'' AFTER `default_policy_group_id`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config' + AND COLUMN_NAME = 'global_friend_enabled') = 0, + 'ALTER TABLE `im_tenant_config` ADD COLUMN `global_friend_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Published global friend switch'' AFTER `security_policy_version`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config' + AND COLUMN_NAME = 'message_curfew_enabled') = 0, + 'ALTER TABLE `im_tenant_config` ADD COLUMN `message_curfew_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''Published message curfew switch'' AFTER `global_friend_enabled`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +-- ============================================================ +-- 5. Invitation-code compatibility for the 6-digit target format. +-- Existing 5-letter codes remain stored and valid during the application +-- rollout. New code generation must switch to exactly six ASCII digits, while +-- Admin and IM Server temporarily accept both legacy 5-letter and new 6-digit +-- values. max_uses=0 preserves the current unlimited-use behavior. +-- ============================================================ + +SET @im_sql = IF( + (SELECT CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_registration_invite_codes' + AND COLUMN_NAME = 'code') = 5, + 'ALTER TABLE `im_registration_invite_codes` MODIFY COLUMN `code` CHAR(6) CHARACTER SET ascii COLLATE ascii_bin NOT NULL', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_registration_invite_codes' + AND COLUMN_NAME = 'max_uses') = 0, + 'ALTER TABLE `im_registration_invite_codes` ADD COLUMN `max_uses` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''0 means unlimited'' AFTER `expires_at`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_registration_invite_batches' + AND COLUMN_NAME = 'max_uses_per_code') = 0, + 'ALTER TABLE `im_registration_invite_batches` ADD COLUMN `max_uses_per_code` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''0 means unlimited'' AFTER `quantity`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +-- ============================================================ +-- 6. Safe global fallback policy. +-- Tenant-specific published policies override tenant_id=0. +-- This keeps existing behavior permissive until an administrator publishes +-- the first tenant policy. +-- ============================================================ + +INSERT INTO `im_security_policy` ( + `tenant_id`, `policy_version`, `policy_name`, `publish_status`, + `global_friend_enabled`, `friend_disabled_policy`, + `message_curfew_enabled`, `remark`, `published_at`, `published_by`, + `created_by`, `updated_by` +) +SELECT + 0, 1, '全局默认安全策略', 'published', + 1, + JSON_OBJECT( + 'hide_profile', TRUE, + 'block_qrcode', TRUE, + 'block_group_member_entry', TRUE, + 'blocked_entries', JSON_ARRAY( + 'search', 'profile_card', 'group_member', + 'qrcode', 'invite_auto_add', 'recommendation' + ), + 'message', '当前时段暂不允许添加好友' + ), + 0, '系统初始化:保持原有好友和消息行为', CURRENT_TIMESTAMP(3), 0, 0, 0 +WHERE NOT EXISTS ( + SELECT 1 FROM `im_security_policy` + WHERE `tenant_id` = 0 AND `publish_status` = 'published' AND `deleted_at` IS NULL +); + +UPDATE `im_security_policy` +SET `friend_disabled_policy` = JSON_SET( + `friend_disabled_policy`, + '$.blocked_entries', + JSON_ARRAY('search', 'profile_card', 'group_member', 'qrcode', 'invite_auto_add', 'recommendation') +) +WHERE `tenant_id` = 0 + AND `publish_status` = 'published' + AND `deleted_at` IS NULL + AND JSON_CONTAINS_PATH(`friend_disabled_policy`, 'one', '$.blocked_entries') = 0; + +-- Post-deployment checks (read-only): +-- SELECT tenant_id, group_code, status, priority FROM im_user_policy_groups; +-- SELECT tenant_id, policy_version, publish_status, +-- global_friend_enabled, message_curfew_enabled +-- FROM im_security_policy ORDER BY tenant_id, policy_version DESC; +-- SELECT tenant_id, policy_id, rule_code, date_type, start_time, end_time, scope +-- FROM im_message_curfew_rules WHERE deleted_at IS NULL; diff --git a/docs/admin-adfahi-research/sql/IM-Server改造.sql b/docs/admin-adfahi-research/sql/IM-Server改造.sql new file mode 100644 index 0000000..3c75193 --- /dev/null +++ b/docs/admin-adfahi-research/sql/IM-Server改造.sql @@ -0,0 +1,417 @@ +-- IM Server schema changes for the GedaDa management-console adaptation. +-- Target: MySQL 8.0+, one independent tenant business database. +-- Default local tenant database: `tinode`. +-- IMPORTANT: execute this migration once for every tenant database. Tenant +-- business tables intentionally do not contain tenant_id. +-- Review before execution. DDL statements auto-commit in MySQL. + +SET NAMES utf8mb4; +USE `tinode`; + +-- ============================================================ +-- 1. Applied runtime policy snapshots. +-- policy_type: security, user_policy_groups, sensitive_words. +-- The application uses the latest row with apply_status=applied and also keeps +-- an in-memory cache. This table makes restart and Kafka replay deterministic. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `im_runtime_policy_snapshots` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `policy_type` varchar(32) NOT NULL, + `policy_version` bigint unsigned NOT NULL, + `payload` json NOT NULL, + `payload_sha256` char(64) NOT NULL, + `source_event_id` varchar(96) DEFAULT NULL COMMENT 'NULL for a manual bootstrap snapshot', + `apply_status` varchar(16) NOT NULL DEFAULT 'applied' COMMENT 'applied or failed', + `apply_error` varchar(1000) NOT NULL DEFAULT '', + `applied_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_runtime_policy_version` (`policy_type`, `policy_version`), + UNIQUE KEY `uk_im_runtime_policy_event` (`source_event_id`), + KEY `idx_im_runtime_policy_latest` (`policy_type`, `apply_status`, `policy_version` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Last applied Admin policy payloads'; + +-- ============================================================ +-- 2. User login history and login failure history. +-- Never store plaintext passwords, verification codes, access tokens or refresh +-- tokens. account_hash is SHA-256 of the normalized login account; account_mask +-- is the value shown in Admin. +-- login_type: password, sms_code, sso, qr_code, token_refresh. +-- event_type: login, refresh, logout, kicked, session_expired. +-- failure_type: password_error, code_error, account_not_found, +-- account_disabled, rate_limited, risk_blocked. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `im_user_login_logs` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `username` varchar(64) NOT NULL DEFAULT '', + `account_type` varchar(24) NOT NULL DEFAULT '', + `account_hash` char(64) NOT NULL DEFAULT '', + `account_mask` varchar(128) NOT NULL DEFAULT '', + `login_type` varchar(24) NOT NULL, + `event_type` varchar(24) NOT NULL DEFAULT 'login', + `session_id` varchar(96) NOT NULL DEFAULT '' COMMENT 'Opaque session identifier, never an auth token', + `device_id` varchar(255) NOT NULL DEFAULT '', + `platform` varchar(32) NOT NULL DEFAULT 'unknown', + `app_version` varchar(32) NOT NULL DEFAULT '', + `ip` varchar(64) NOT NULL DEFAULT '', + `region` varchar(128) NOT NULL DEFAULT '', + `user_agent` varchar(512) NOT NULL DEFAULT '', + `risk_level` varchar(16) NOT NULL DEFAULT 'low', + `request_id` varchar(64) NOT NULL DEFAULT '', + `login_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `logout_at` datetime(3) DEFAULT NULL, + `metadata` json DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_im_user_login_user_time` (`user_id`, `login_at` DESC, `id` DESC), + KEY `idx_im_user_login_ip_time` (`ip`, `login_at` DESC, `id` DESC), + KEY `idx_im_user_login_device_time` (`device_id`(64), `login_at` DESC), + KEY `idx_im_user_login_request` (`request_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='User login and session event history'; + +CREATE TABLE IF NOT EXISTS `im_login_failure_logs` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint DEFAULT NULL COMMENT 'NULL when the account cannot be resolved', + `account_type` varchar(24) NOT NULL DEFAULT '', + `account_hash` char(64) NOT NULL DEFAULT '', + `account_mask` varchar(128) NOT NULL DEFAULT '', + `failure_type` varchar(32) NOT NULL, + `reason` varchar(96) NOT NULL DEFAULT '' COMMENT 'Stable backend reason code', + `detail` varchar(500) NOT NULL DEFAULT '' COMMENT 'Sanitized detail without credentials', + `ip` varchar(64) NOT NULL DEFAULT '', + `region` varchar(128) NOT NULL DEFAULT '', + `device_id` varchar(255) NOT NULL DEFAULT '', + `platform` varchar(32) NOT NULL DEFAULT 'unknown', + `app_version` varchar(32) NOT NULL DEFAULT '', + `user_agent` varchar(512) NOT NULL DEFAULT '', + `risk_level` varchar(16) NOT NULL DEFAULT 'medium', + `risk_action` varchar(24) NOT NULL DEFAULT 'audit_only', + `request_id` varchar(64) NOT NULL DEFAULT '', + `attempt_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `metadata` json DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_im_login_failure_account_time` (`account_hash`, `attempt_at` DESC, `id` DESC), + KEY `idx_im_login_failure_user_time` (`user_id`, `attempt_at` DESC, `id` DESC), + KEY `idx_im_login_failure_ip_time` (`ip`, `attempt_at` DESC, `id` DESC), + KEY `idx_im_login_failure_type_time` (`failure_type`, `attempt_at` DESC, `id` DESC), + KEY `idx_im_login_failure_request` (`request_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Authentication failure and risk-block history'; + +-- ============================================================ +-- 3. Message administration actions. +-- Messages remain in im_messages/im_group_messages. This table is an immutable +-- action overlay so hide/delete can be audited and restored. +-- conversation_type: private, group. +-- action: hide, unhide, delete, restore. +-- action_status: pending, success, failed, dead. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `im_message_admin_actions` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `action_id` varchar(96) NOT NULL, + `command_id` varchar(96) NOT NULL, + `conversation_type` varchar(16) NOT NULL, + `topic` varchar(64) NOT NULL, + `seqid` bigint NOT NULL, + `sender_user_id` bigint DEFAULT NULL, + `action` varchar(16) NOT NULL, + `action_status` varchar(16) NOT NULL DEFAULT 'pending', + `reason` varchar(500) NOT NULL DEFAULT '', + `actor_admin_id` bigint NOT NULL DEFAULT 0, + `request_id` varchar(64) NOT NULL DEFAULT '', + `before_snapshot` json DEFAULT NULL, + `after_snapshot` json DEFAULT NULL, + `failure_reason` varchar(96) NOT NULL DEFAULT '', + `failure_message` varchar(1000) NOT NULL DEFAULT '', + `processed_at` datetime(3) DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_message_admin_action_id` (`action_id`), + UNIQUE KEY `uk_im_message_admin_command` (`command_id`), + KEY `idx_im_message_admin_target` (`conversation_type`, `topic`, `seqid`, `id` DESC), + KEY `idx_im_message_admin_status` (`action_status`, `created_at`, `id`), + KEY `idx_im_message_admin_actor` (`actor_admin_id`, `created_at` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Auditable Admin message actions'; + +-- ============================================================ +-- 4. Sensitive-word hits and policy decisions. +-- Store only a digest and a short masked excerpt. Do not duplicate complete +-- private messages in log tables. +-- policy decision reasons include FRIEND_DISABLED and MESSAGE_CURFEW_ACTIVE. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `im_sensitive_word_hits` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `word_id` bigint NOT NULL DEFAULT 0 COMMENT 'Admin control-database word ID', + `word_group_id` bigint NOT NULL DEFAULT 0, + `word_snapshot` varchar(255) NOT NULL DEFAULT '' COMMENT 'Masked word snapshot', + `scene` varchar(32) NOT NULL, + `user_id` bigint NOT NULL DEFAULT 0, + `target_user_id` bigint DEFAULT NULL, + `topic` varchar(64) NOT NULL DEFAULT '', + `seqid` bigint DEFAULT NULL, + `content_sha256` char(64) NOT NULL DEFAULT '', + `matched_excerpt` varchar(500) NOT NULL DEFAULT '', + `match_type` varchar(24) NOT NULL DEFAULT 'contains', + `action` varchar(24) NOT NULL, + `risk_level` varchar(16) NOT NULL DEFAULT 'medium', + `request_id` varchar(64) NOT NULL DEFAULT '', + `metadata` json DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_im_sensitive_hit_scene_time` (`scene`, `created_at` DESC, `id` DESC), + KEY `idx_im_sensitive_hit_user_time` (`user_id`, `created_at` DESC, `id` DESC), + KEY `idx_im_sensitive_hit_target` (`topic`, `seqid`, `id` DESC), + KEY `idx_im_sensitive_hit_word` (`word_id`, `created_at` DESC), + KEY `idx_im_sensitive_hit_request` (`request_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Sensitive-word match events'; + +CREATE TABLE IF NOT EXISTS `im_policy_decision_logs` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `policy_type` varchar(32) NOT NULL COMMENT 'security or user_policy_group', + `policy_version` bigint unsigned NOT NULL DEFAULT 0, + `policy_group_id` bigint NOT NULL DEFAULT 0, + `scene` varchar(64) NOT NULL COMMENT 'friend_request, qrcode_resolve, private_message and so on', + `action` varchar(24) NOT NULL COMMENT 'deny, challenge or audit_only', + `reason_code` varchar(96) NOT NULL COMMENT 'For example FRIEND_DISABLED or MESSAGE_CURFEW_ACTIVE', + `user_id` bigint NOT NULL DEFAULT 0, + `target_user_id` bigint DEFAULT NULL, + `topic` varchar(64) NOT NULL DEFAULT '', + `request_id` varchar(64) NOT NULL DEFAULT '', + `ip` varchar(64) NOT NULL DEFAULT '', + `metadata` json DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_im_policy_decision_user_time` (`user_id`, `created_at` DESC, `id` DESC), + KEY `idx_im_policy_decision_reason_time` (`reason_code`, `created_at` DESC, `id` DESC), + KEY `idx_im_policy_decision_scene_time` (`scene`, `created_at` DESC, `id` DESC), + KEY `idx_im_policy_decision_request` (`request_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Runtime policy deny/challenge decisions'; + +-- ============================================================ +-- 5. Group event history and daily IP/account statistics. +-- im_group_events_outbox remains the delivery table; this log is the queryable +-- business history after the operation result is known. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `im_group_event_logs` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `event_id` varchar(96) NOT NULL, + `topic` varchar(64) NOT NULL, + `group_id` bigint DEFAULT NULL, + `event_type` varchar(64) NOT NULL, + `actor_user_id` bigint NOT NULL DEFAULT 0, + `target_user_id` bigint DEFAULT NULL, + `result` varchar(16) NOT NULL DEFAULT 'success', + `reason` varchar(500) NOT NULL DEFAULT '', + `request_id` varchar(64) NOT NULL DEFAULT '', + `before_snapshot` json DEFAULT NULL, + `after_snapshot` json DEFAULT NULL, + `metadata` json DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_group_event_log_event` (`event_id`), + KEY `idx_im_group_event_topic_time` (`topic`, `created_at` DESC, `id` DESC), + KEY `idx_im_group_event_type_time` (`event_type`, `created_at` DESC, `id` DESC), + KEY `idx_im_group_event_actor_time` (`actor_user_id`, `created_at` DESC, `id` DESC), + KEY `idx_im_group_event_request` (`request_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Queryable group event audit history'; + +CREATE TABLE IF NOT EXISTS `im_ip_account_stats_daily` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `stat_date` date NOT NULL, + `ip` varchar(64) NOT NULL, + `region` varchar(128) NOT NULL DEFAULT '', + `account_count` int unsigned NOT NULL DEFAULT 0, + `user_count` int unsigned NOT NULL DEFAULT 0, + `login_success_count` int unsigned NOT NULL DEFAULT 0, + `login_failure_count` int unsigned NOT NULL DEFAULT 0, + `first_seen_at` datetime(3) DEFAULT NULL, + `last_seen_at` datetime(3) DEFAULT NULL, + `risk_level` varchar(16) NOT NULL DEFAULT 'low', + `risk_action` varchar(24) NOT NULL DEFAULT 'audit_only', + `sample_user_ids` json DEFAULT NULL COMMENT 'Bounded sample only, not an unlimited array', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_ip_account_stats_day_ip` (`stat_date`, `ip`), + KEY `idx_im_ip_account_stats_ip_day` (`ip`, `stat_date` DESC), + KEY `idx_im_ip_account_stats_risk_day` (`risk_level`, `stat_date` DESC, `id` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Daily IP and account aggregation'; + +-- ============================================================ +-- 6. Compatible enhancements to existing tables. +-- Every ALTER is guarded by information_schema and can be rerun. +-- ============================================================ + +-- Message management queries need topic/time and sender/time indexes. +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_messages' + AND INDEX_NAME = 'idx_im_messages_topic_created') = 0, + 'ALTER TABLE `im_messages` ADD INDEX `idx_im_messages_topic_created` (`topic`, `createdat`, `id`)', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_messages' + AND INDEX_NAME = 'idx_im_messages_from_created') = 0, + 'ALTER TABLE `im_messages` ADD INDEX `idx_im_messages_from_created` (`from`, `createdat`, `id`)', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +-- Extend the existing Tinode device record for device and login-history views. +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' + AND COLUMN_NAME = 'firstseen') = 0, + 'ALTER TABLE `im_devices` ADD COLUMN `firstseen` DATETIME(3) DEFAULT NULL AFTER `platform`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' + AND COLUMN_NAME = 'lastip') = 0, + 'ALTER TABLE `im_devices` ADD COLUMN `lastip` VARCHAR(64) NOT NULL DEFAULT '''' AFTER `lastseen`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' + AND COLUMN_NAME = 'useragent') = 0, + 'ALTER TABLE `im_devices` ADD COLUMN `useragent` VARCHAR(512) NOT NULL DEFAULT '''' AFTER `lastip`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' + AND COLUMN_NAME = 'appversion') = 0, + 'ALTER TABLE `im_devices` ADD COLUMN `appversion` VARCHAR(32) NOT NULL DEFAULT '''' AFTER `useragent`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices' + AND COLUMN_NAME = 'status') = 0, + 'ALTER TABLE `im_devices` ADD COLUMN `status` VARCHAR(16) NOT NULL DEFAULT ''active'' AFTER `appversion`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +-- Feedback handling fields required by the Admin processing workflow. +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' + AND COLUMN_NAME = 'assigned_to') = 0, + 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `assigned_to` BIGINT DEFAULT NULL AFTER `status`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' + AND COLUMN_NAME = 'handled_by') = 0, + 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `handled_by` BIGINT DEFAULT NULL AFTER `assigned_to`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' + AND COLUMN_NAME = 'handled_at') = 0, + 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `handled_at` DATETIME(3) DEFAULT NULL AFTER `handled_by`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' + AND COLUMN_NAME = 'resolution') = 0, + 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `resolution` VARCHAR(1000) NOT NULL DEFAULT '''' AFTER `handled_at`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' + AND COLUMN_NAME = 'admin_remark') = 0, + 'ALTER TABLE `im_feedback_submissions` ADD COLUMN `admin_remark` VARCHAR(1000) NOT NULL DEFAULT '''' AFTER `resolution`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions' + AND INDEX_NAME = 'idx_im_feedback_status_time') = 0, + 'ALTER TABLE `im_feedback_submissions` ADD INDEX `idx_im_feedback_status_time` (`status`, `created_at`, `id`)', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_audit_logs' + AND INDEX_NAME = 'idx_im_audit_logs_ip_time') = 0, + 'ALTER TABLE `im_audit_logs` ADD INDEX `idx_im_audit_logs_ip_time` (`ip`, `created_at`, `id`)', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +-- Post-deployment checks (read-only): +-- SELECT policy_type, policy_version, apply_status, applied_at +-- FROM im_runtime_policy_snapshots ORDER BY policy_type, policy_version DESC; +-- SELECT reason_code, scene, COUNT(*) AS deny_count +-- FROM im_policy_decision_logs +-- WHERE created_at >= CURRENT_DATE +-- GROUP BY reason_code, scene; +-- SELECT stat_date, ip, account_count, login_failure_count, risk_level +-- FROM im_ip_account_stats_daily ORDER BY stat_date DESC, login_failure_count DESC; diff --git a/docs/admin-adfahi-research/需求文档.md b/docs/admin-adfahi-research/需求文档.md new file mode 100644 index 0000000..cd70c8e --- /dev/null +++ b/docs/admin-adfahi-research/需求文档.md @@ -0,0 +1,1462 @@ +# 鸽达达管理后台复刻需求文档 + +调研对象:https://admin.adfahi.cn +调研时间:2026-09-04 +当前登录账号视角:`heiyue` +目标:在本项目中复刻一套功能与页面结构一致的 IM 管理后台。 + +> 说明:本次通过内置浏览器逐项点击左侧菜单,进入页面后对有查询按钮的页面执行了“查询/查 询”,再采集当前可视区域截图、可见字段、表格列、操作按钮,并下载前端 bundle 反查接口路径。浏览器只读执行环境屏蔽了 `fetch/XMLHttpRequest`,因此未能直接读取接口 JSON body;本文档中的返回字段以页面查询后的渲染结果和前端接口定义反推。 + +## 0. 本次修订说明 + +根据补充要求,已做以下修订: + +- 截图从原先整页长图改为“当前可视区域截图”,减少大面积空白,更接近实际浏览器视图。 +- 每个列表页进入后均点击查询按钮;`群成员管理` 因群号必填,使用群组列表中已有群号 `68127154` 查询,已展示成员数据。 +- 单聊消息、群聊消息、文件管理、日志审计、用户登录、IP 统计、登录失败日志等页面查询后均展示了数据。 +- 邀请码管理已查询到数据;敏感词库、用户反馈当前查询结果为空,需求文档中按“需支持空态”记录。 +- 文档已在每个 `3.x` 页面章节内补充接口字段、查询参数、返回字段、按钮功能、表格字段、枚举、业务动作、危险操作确认、导入导出、分页和空态说明。 +- 新截图保存在 `docs/admin-adfahi-research/screenshots-queried/`。 + +## 1. 总体信息架构 + +左侧菜单分为 5 个一级模块,当前账号可见 20 个页面: + +| 一级模块 | 页面 | 前端路由 | +| --- | --- | --- | +| 数据概览 | 数据概览 | `/dashboard` | +| 用户与群组 | 用户管理 | `/users` | +| 用户与群组 | 用户分组 | `/user-policy-groups` | +| 用户与群组 | 群组管理 | `/groups` | +| 用户与群组 | 群成员管理 | `/group-members` | +| 用户与群组 | 邀请码管理 | `/invite-codes` | +| 内容审核 | 单聊消息 | `/private-messages` | +| 内容审核 | 群聊消息 | `/group-messages` | +| 内容审核 | 敏感词库 | `/sensitive-words` | +| 内容审核 | 文件管理 | `/file-records` | +| 内容审核 | 用户反馈 | `/feedbacks` | +| 日志审计 | 操作日志 | `/audit-logs` | +| 日志审计 | 用户账号审计 | `/user-account-audit` | +| 日志审计 | 群事件审计 | `/group-events` | +| 日志审计 | 管理员登录 | `/admin-login-history` | +| 日志审计 | 用户登录 | `/user-login-history` | +| 日志审计 | IP 账号统计 | `/ip-user-stats` | +| 日志审计 | 登录失败日志 | `/login-failure-logs` | +| 系统设置 | 私域设置 | `/tenant-settings` | +| 系统设置 | 安全策略 | `/security-settings` | + +菜单总览截图: + +![菜单总览](screenshots-queried/00-menu-overview.png) + +## 2. 全局布局与交互 + +后台采用典型管理系统布局: + +- 左侧固定菜单,支持一级菜单展开/收起。 +- 顶部显示当前账号头像、用户名、账户操作入口。 +- 主区域顶部有多标签页导航,打开过的页面会以 tab 形式保留,可关闭。 +- 列表页统一具备查询区、操作按钮区、表格、分页。 +- 详情、编辑、重置密码、IP 白名单、处理反馈、分组设置等操作预计以弹窗完成。 +- 危险操作需要二次确认,包括封禁、强制下线、强制注销、解散群、删除文件、删除敏感词、解锁账号等。 + +建议在本项目中按 HotGo 规范实现:后端 GoFrame 分层,前端 Vue 3 + Naive UI,枚举/字典由后端统一维护,前端不硬编码字典文本。 + +## 3. 页面需求 + +### 3.1 数据概览 + +路由:`/dashboard` +接口:`GET /admin/stats` + +页面展示统计卡片和趋势表: + +- 总用户数:16 +- 活跃用户:0 +- 总群组数:6 +- 总会话数:11 +- 总消息数:220 +- 今日消息:0 +- 用户增长趋势:日期、数量 +- 消息增长趋势:日期、数量 + +字段与数据口径: + +| 字段 | 接口字段建议 | 含义 | 展示/统计规则 | +| --- | --- | --- | --- | +| 总用户数 | `total_users` | 当前租户用户总量 | 建议统计未物理删除用户,可包含正常、封禁、注销 | +| 活跃用户 | `active_users` | 近期活跃用户 | 建议按当天或近 7 天登录/发消息口径统计 | +| 总群组数 | `total_groups` | 当前租户群组总量 | 建议包含正常和封禁群,已解散群是否计入需配置 | +| 总会话数 | `total_conversations` | 单聊和群聊会话总数 | 按会话去重统计 | +| 总消息数 | `total_messages` | 历史消息总量 | 包含文本、图片、视频、语音、文件、名片、系统消息 | +| 今日消息 | `today_messages` | 当日新增消息数 | 按服务器时区从 00:00 统计 | +| 用户增长趋势 | `user_growth[]` | 最近 7 天新增用户 | 元素包含 `date`、`count` | +| 消息增长趋势 | `message_growth[]` | 最近 7 天新增消息 | 元素包含 `date`、`count` | + +按钮与交互: + +- 本页无查询按钮,进入页面自动调用统计接口。 +- 统计卡片为空时数字显示 `0`,趋势表仍展示日期行,数量为 `0`。 +- 页面需支持后续扩展为折线图,但必须保留日期和数量明细。 + +截图: + +![数据概览](screenshots-queried/01-dashboard.png) + +### 3.2 用户管理 + +路由:`/users` +核心接口: + +- `GET /admin/users` +- `POST /admin/users` +- `GET /admin/users/{id}` +- `PATCH /admin/users/{id}/status` +- `POST /admin/users/{id}/reset_password` +- `POST /admin/users/{id}/ip-whitelist` +- `POST /admin/users/{id}/kick` +- `POST /admin/users/{id}/deactivate` +- `POST /admin/users/batch` +- `POST /admin/users/batch-ban` +- `POST /admin/users/ban-by-ip` +- `POST /admin/users/import-xlsx` + +查询条件: + +- 用户编号 +- 用户分组 +- 状态 +- 关键词:用户名、昵称、手机号 + +顶部操作: + +- 创建用户 +- 批量创建 +- 导入 Excel +- 下载模板 + +表格列: + +- 编号 +- 头像 +- 用户名 +- 昵称 +- 手机号 +- 注册方式 +- 状态 +- 用户分组 +- 最近登录 +- 创建时间 +- 操作 + +行操作: + +- 详情 +- 重置密码 +- IP 白名单 +- 调整分组 +- 封禁 +- 强制下线 +- 强制注销 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按条件筛选用户 | `GET /admin/users` | 点击后页码回到第 1 页;支持编号、分组、状态、关键词组合查询 | +| 重 置 | 清空筛选条件 | `GET /admin/users` | 清空编号、用户分组、状态、关键词后重新查询 | +| 创建用户 | 后台新增单个用户 | `POST /admin/users` | 弹窗填写昵称、手机号、密码、用户分组;平台账号还需选择租户;密码受安全策略限制 | +| 批量创建 | 批量生成用户 | `POST /admin/users/batch` | 支持批量数量、默认密码、默认分组;完成后展示成功/失败并支持下载结果 | +| 导入Excel | Excel 批量导入用户 | `POST /admin/users/import-xlsx` | 上传 `.xlsx`;模板列为昵称、手机号、密码;返回成功数、失败数、失败原因 | +| 下载模板 | 下载导入模板 | 静态生成或导出接口 | 模板示例字段:昵称、手机号、密码 | +| 详 情 | 查看用户详情 | `GET /admin/users/{id}` | 展示基础资料、状态、分组、最近登录、IP 白名单、安全权限等 | +| 重置密码 | 管理员重置密码 | `POST /admin/users/{id}/reset_password` | 弹窗输入新密码或生成密码;成功后写用户账号审计 | +| IP白名单 | 设置用户登录白名单 | `POST /admin/users/{id}/ip-whitelist` | 多行 IP/CIDR;用户白名单不覆盖全局登录 IP 黑名单 | +| 调整分组 | 修改用户策略组 | 用户分组分配接口 | 立即影响好友、建群、APP/Web 登录等权限 | +| 封 禁 / 解封 | 切换用户状态 | `PATCH /admin/users/{id}/status` | 封禁后撤销在线会话并禁止登录;需二次确认 | +| 强制下线 | 踢出在线终端 | `POST /admin/users/{id}/kick` | 不改变用户状态,仅撤销当前登录态 | +| 强制注销 | 注销用户账号 | `POST /admin/users/{id}/deactivate` | 高危操作,需二次确认;注销后按规则清理用户数据 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 编号 | `user_no` | 对外展示的用户编号 | 数字,支持精确查询 | +| 头像 | `avatar` | 用户头像 URL | 显示缩略图;无头像显示默认头像;支持点击查看 | +| 用户名 | `username` | 用户登录名/IM 账号 | 样例:`hy73074162` | +| 昵称 | `nickname` | 用户显示名 | 允许为空,空值显示 `--` | +| 手机号 | `mobile` / `phone` | 绑定手机号 | 建议脱敏展示 | +| 注册方式 | `register_type` | 用户来源 | 后台创建、手机号注册、邀请码注册 | +| 状态 | `status` | 账号状态 | 正常绿色、封禁红色、注销灰色 | +| 用户分组 | `policy_group_name` | 所属策略组 | 样例:默认用户组 | +| 最近登录 | `last_login_at` | 最近一次登录时间 | 无记录显示 `--` | +| 创建时间 | `created_at` | 用户创建时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 用户状态:`active` 正常、`disabled` 已封禁、`deleted` 已注销。 +- 注册方式:`admin` 后台创建、`mobile` 手机号注册、`invite_code` 邀请码注册。 +- 批量封禁结果:`success` 成功、`failed` 失败,失败项需返回 `username` 和 `reason`。 + +截图: + +![用户管理](screenshots-queried/02-user-management.png) + +### 3.3 用户分组 + +路由:`/user-policy-groups` +核心接口: + +- `GET /admin/user-policy-groups` +- `POST /admin/user-policy-groups` +- `PUT /admin/user-policy-groups/{id}` +- `DELETE /admin/user-policy-groups/{id}` +- `GET /admin/user-policy-groups/{id}/users` +- `GET/POST /admin/user-policy-groups/ip-whitelist` + +顶部操作: + +- 添加分组 +- IP 白名单 + +表格列: + +- 分组名称 +- 用户数 +- 允许加好友 +- 允许建群 +- 直接加好友 +- 好友上限 +- APP 登录 +- Web 登录 +- IP 白名单 +- 状态 +- 权重 +- 操作 + +编辑分组字段: + +- 分组名称 +- 默认用户组 +- 启用状态 +- 允许加好友 +- 允许建群 +- 直接添加好友 +- 允许 APP 登录 +- 允许 Web 登录 +- 启用 IP 白名单 +- 好友数量上限,0 表示不限 +- 排序权重 + +IP 白名单字段: + +- APP 登录白名单,每行一个 IPv4、IPv6 或 CIDR +- Web 登录白名单,每行一个 IPv4、IPv6 或 CIDR + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 添加分组 | 新建用户策略组 | `POST /admin/user-policy-groups` | 弹窗填写分组名称、开关权限、好友上限、排序权重 | +| IP 白名单 | 设置租户渠道白名单 | `GET/POST /admin/user-policy-groups/ip-whitelist` | 分 APP 登录白名单和 Web 登录白名单,每行一个 IP/CIDR | +| 编辑 | 修改策略组 | `PUT /admin/user-policy-groups/{id}` | 默认分组部分字段需禁用,避免系统无默认组 | +| 删除 | 删除策略组 | `DELETE /admin/user-policy-groups/{id}` | 默认用户组不可删除;分组下有用户时应阻止或要求迁移 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 分组名称 | `name` | 策略组名称 | 默认组显示“默认”标记 | +| 用户数 | `user_count` | 分组下用户数量 | 可扩展点击查看用户列表 | +| 允许加好友 | `allow_add_friend` | 是否允许主动加好友 | 允许/禁止 | +| 允许建群 | `allow_create_group` | 是否允许创建群 | 允许/禁止 | +| 直接加好友 | `direct_add_friend` | 加好友是否免确认 | 开启显示开启,关闭显示 `--` | +| 好友上限 | `max_friend_count` | 最大好友数量 | `0` 显示不限 | +| APP 登录 | `allow_app_login` | 是否允许 APP 登录 | 允许/禁止 | +| Web 登录 | `allow_web_login` | 是否允许 Web 登录 | 允许/禁止 | +| IP 白名单 | `ip_whitelist_enabled` | 是否启用分组白名单 | 已启用/未启用 | +| 状态 | `enabled` | 分组是否生效 | 启用/停用 | +| 权重 | `sort_order` | 排序或匹配优先级 | 数字 | + +枚举说明: + +- 布尔权限统一为 `true/false`,前端按字段语义显示为允许/禁止、启用/停用、开启/关闭。 +- 好友上限 `0` 代表不限。 +- IP 白名单启用后,APP/Web 登录需命中对应白名单;全局黑名单优先级高于白名单。 + +截图: + +![用户分组](screenshots-queried/03-user-groups.png) + +### 3.4 群组管理 + +路由:`/groups` +核心接口: + +- `GET /admin/groups` +- `GET /admin/groups/{id}` +- `POST /admin/groups/{id}/ban` +- `POST /admin/groups/{id}/unban` +- `DELETE /admin/groups/{id}` +- `GET /admin/groups/{id}/members` +- `GET /admin/groups/{id}/files` +- `DELETE /admin/groups/{id}/files/{fileId}` +- `POST /admin/groups/quick-create` + +查询条件: + +- 群号 +- 关键词:群名称 +- 状态 + +表格列: + +- 群组编号 +- 群号 +- 群组名称 +- 成员数 +- 群主昵称 +- 群主用户名 +- 状态 +- 创建时间 +- 操作 + +行操作: + +- 详情 +- 群文件 +- 封禁 +- 解散 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按群号、群名、状态筛选 | `GET /admin/groups` | 点击后页码回到第 1 页 | +| 重 置 | 清空筛选条件 | `GET /admin/groups` | 清空群号、关键词、状态后重新查询 | +| 详 情 | 查看群详情 | `GET /admin/groups/{id}` | 展示群号、群名、群主、成员数、状态、创建时间 | +| 群文件 | 查看群文件 | `GET /admin/groups/{id}/files` | 弹窗列表支持类型筛选、下载、删除 | +| 封 禁 / 解封 | 切换群状态 | `POST /admin/groups/{id}/ban`、`POST /admin/groups/{id}/unban` | 封禁后群内不可发送消息;需记录群事件审计 | +| 解 散 | 解散群聊 | `DELETE /admin/groups/{id}` | 高危操作,需二次确认;清理成员关系,保留历史记录 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 群组编号 | `id` | 群内部编号 | 数字 | +| 群号 | `group_no` / `group_code` | 用户端可搜索群号 | 支持精确查询 | +| 群组名称 | `name` | 群显示名称 | 样例:冯、冯氏家族 | +| 成员数 | `member_count` | 当前成员数量 | 数字 | +| 群主昵称 | `owner_nickname` | 群主昵称 | 空值显示 `--` | +| 群主用户名 | `owner_username` | 群主用户名 | 样例:`hy69089908` | +| 状态 | `status` | 群状态 | 正常/封禁/已解散 | +| 创建时间 | `created_at` | 群创建时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 群组状态:`normal` 正常、`banned` 已封禁、`dissolved` 已解散。 +- 群文件类型:图片、视频、音频、文档、其他。 + +截图: + +![群组管理](screenshots-queried/04-group-management.png) + +### 3.5 群成员管理 + +路由:`/group-members` +接口依赖: + +- `GET /admin/groups/{id}/members` +- `POST /admin/groups/{id}/members/kick` +- `POST /admin/groups/{id}/members/mute` +- `POST /admin/groups/{id}/members/unmute` + +查询条件: + +- 群号,必填 +- 角色 +- 关键词:用户名、昵称 + +表格列: + +- 编号 +- 用户名 +- 昵称 +- 群昵称 +- 角色 +- 禁言状态 +- 加入时间 +- 操作 + +顶部操作: + +- 查询 +- 重置 +- 刷新 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按群号查询成员 | `GET /admin/groups/{id}/members` | 群号必填;需先按群号解析群 ID | +| 重 置 | 清空群号、角色、关键词 | - | 清空后表格回到空态 | +| 刷 新 | 保持当前条件刷新 | `GET /admin/groups/{id}/members` | 不清空已输入群号 | +| 禁 言 / 解除禁言 | 设置成员禁言状态 | `POST /admin/groups/{id}/members/mute`、`POST /admin/groups/{id}/members/unmute` | 需填写禁言时长/原因;写群事件审计 | +| 踢 出 | 将成员移出群 | `POST /admin/groups/{id}/members/kick` | 需二次确认;群主不可直接踢出 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 编号 | `user_no` | 成员用户编号 | 样例:3 | +| 用户名 | `username` | 成员用户名 | 样例:`hy69089908` | +| 昵称 | `nickname` | 成员昵称 | 样例:F | +| 群昵称 | `group_nickname` | 群内昵称 | 空值显示 `--` | +| 角色 | `role` | 群角色 | 群主/管理员/成员 | +| 禁言状态 | `mute_status` | 是否禁言 | 正常/已禁言 | +| 加入时间 | `joined_at` | 入群时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 群成员角色:`owner` 群主、`admin` 管理员、`member` 成员。 +- 禁言状态:`normal` 正常、`muted` 已禁言。 +- 本次查询样例使用群号 `68127154`,查询到成员 `hy69089908 / F`,角色为群主。 + +截图: + +![群成员管理](screenshots-queried/05-group-members.png) + +### 3.6 邀请码管理 + +路由:`/invite-codes` +核心接口: + +- `GET /admin/invite-codes` +- `POST /admin/invite-codes` +- `DELETE /admin/invite-codes/{id}` +- `GET /admin/invite-codes/{id}/usages` + +查询条件: + +- 状态 +- 邀请码,6 位数字 + +顶部操作: + +- 生成邀请码 + +业务说明: + +- 每次生成 1 个邀请码,可多次使用。 +- 可指定企业与用户。 +- 可勾选不限制时间,永久有效。 +- 可开启自动加好友,使用该码注册的用户直接成为指定用户好友。 +- 可查看使用该邀请码注册的用户列表。 + +表格列: + +- 邀请码 +- 指定用户 +- 自动加好友 +- 使用人数 +- 创建人 +- 状态 +- 过期时间 +- 创建时间 +- 操作 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按状态/邀请码筛选 | `GET /admin/invite-codes` | 邀请码为 6 位数字 | +| 重 置 | 清空状态和邀请码 | `GET /admin/invite-codes` | 回到全部列表 | +| 生成邀请码 | 创建邀请码 | `POST /admin/invite-codes` | 可指定用户、有效期、是否自动加好友;生成 6 位数字码 | +| 查 看 | 查看使用记录 | `GET /admin/invite-codes/{id}/usages` | 展示使用该码注册的用户列表 | +| 删 除 | 删除/停用邀请码 | `DELETE /admin/invite-codes/{id}` | 已使用邀请码建议标记停用,不物理删除 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 邀请码 | `code` | 6 位数字邀请码 | 样例:323441 | +| 指定用户 | `target_username` / `target_user_no` | 注册后绑定或自动加好友目标 | 样例:往事随风 (#2271) | +| 自动加好友 | `auto_add_friend` | 是否自动成为指定用户好友 | 是/否 | +| 使用人数 | `used_count` | 已使用注册人数 | 可点击查看详情 | +| 创建人 | `creator_name` / `creator_id` | 创建者 | 当前样例显示编号 16 | +| 状态 | `status` | 邀请码状态 | 启用/停用/过期 | +| 过期时间 | `expired_at` | 有效期截止时间 | 永久显示“不限制”或“永久” | +| 创建时间 | `created_at` | 创建时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 邀请码状态:`active` 启用、`disabled` 停用、`expired` 过期。 +- 自动加好友:`true` 是、`false` 否。 + +截图: + +![邀请码管理](screenshots-queried/06-invite-codes.png) + +### 3.7 单聊消息 + +路由:`/private-messages` +核心接口: + +- `GET /admin/messages/search` +- `GET /admin/messages/{id}` + +查询条件: + +- 消息类型:全部、文本、图片、视频、语音、文件、名片、系统 +- 发送人用户名 +- 接收人用户名 +- 关键词 +- 开始时间 +- 结束时间 + +表格列: + +- 发送人 +- 接收人 +- 类型 +- 内容 +- 状态 +- 发送时间 +- 操作 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按消息条件检索 | `GET /admin/messages/search` | 查询参数含 `conversation_type=private` | +| 重 置 | 清空筛选条件 | `GET /admin/messages/search` | 清空消息类型、发送人、接收人、关键词、时间 | +| 刷 新 | 保持当前条件刷新 | `GET /admin/messages/search` | 不改变页码和筛选条件 | +| 删 除 | 删除或隐藏消息 | 消息删除接口 | 需二次确认;写内容审核/操作日志 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 发送人 | `sender_nickname`、`sender_username` | 发送方用户 | 两行展示昵称和用户名 | +| 接收人 | `receiver_nickname`、`receiver_username` | 接收方用户 | 两行展示昵称和用户名 | +| 类型 | `message_type` | 消息类型 | 文本/图片/视频/语音/文件/名片/系统 | +| 内容 | `content` | 消息摘要 | 文本直接展示,媒体显示预览/下载 | +| 状态 | `status` | 消息状态 | 正常/已删除/已撤回/拦截 | +| 发送时间 | `sent_at` | 消息发送时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 消息类型:`text` 文本、`image` 图片、`video` 视频、`voice/audio` 语音、`file` 文件、`card` 名片、`system` 系统。 +- 消息状态:`normal` 正常、`deleted` 后台删除、`recalled` 用户撤回、`blocked` 风控拦截。 +- 查询后样例:`F / hy69089908` 给 `冯仁飞 / hy91144536` 发送文本 `哈哈哈哈`。 + +截图: + +![单聊消息](screenshots-queried/07-direct-messages.png) + +### 3.8 群聊消息 + +路由:`/group-messages` +核心接口: + +- `GET /admin/messages/search` +- `GET /admin/messages/{id}` + +查询条件: + +- 消息类型:全部、文本、图片、视频、语音、文件、名片、系统 +- 发送人用户名 +- 关键词 +- 开始时间 +- 结束时间 + +表格列: + +- 发送人 +- 群名称 +- 类型 +- 内容 +- 状态 +- 发送时间 +- 操作 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按群聊消息条件检索 | `GET /admin/messages/search` | 查询参数含 `conversation_type=group` | +| 重 置 | 清空筛选条件 | `GET /admin/messages/search` | 清空消息类型、发送人、关键词、时间 | +| 刷 新 | 保持当前条件刷新 | `GET /admin/messages/search` | 不改变页码和筛选条件 | +| 删 除 | 删除或隐藏群消息 | 消息删除接口 | 需二次确认;建议写群事件/内容审核日志 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 发送人 | `sender_nickname`、`sender_username` | 发送方用户 | 两行展示 | +| 群名称 | `group_name`、`group_no` | 消息所属群 | 两行展示群名和群号 | +| 类型 | `message_type` | 消息类型 | 文本/图片/视频/语音/文件/名片/系统 | +| 内容 | `content` | 消息摘要 | 文本展示内容,媒体展示预览/下载 | +| 状态 | `status` | 消息状态 | 正常/已删除/已撤回/拦截 | +| 发送时间 | `sent_at` | 发送时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 消息类型和消息状态同单聊消息。 +- 查询后样例:`F / hy69089908` 在群 `冯 / 68127154` 发送文本 `哈哈哈哈`。 + +截图: + +![群聊消息](screenshots-queried/08-group-messages.png) + +### 3.9 敏感词库 + +路由:`/sensitive-words` +核心接口: + +- `GET /admin/sensitive-words` +- `POST /admin/sensitive-words` +- `PUT /admin/sensitive-words/{id}` +- `DELETE /admin/sensitive-words/{id}` +- `POST /admin/sensitive-words/import` +- `GET /admin/sensitive-words/export` + +查询条件: + +- 状态 +- 等级 +- 关键词:敏感词 + +顶部操作: + +- 新增敏感词 +- 批量导入 +- 文件导入 +- 批量导出 + +表格列: + +- ID +- 敏感词 +- 等级 +- 替换词 +- 状态 +- 创建时间 +- 操作 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按状态、等级、关键词筛选 | `GET /admin/sensitive-words` | 点击后页码回到第 1 页 | +| 重 置 | 清空筛选条件 | `GET /admin/sensitive-words` | 回到全部词库 | +| 新增敏感词 | 新增单个词条 | `POST /admin/sensitive-words` | 弹窗填写敏感词、等级、替换词、状态 | +| 批量导入 | 粘贴多行导入 | `POST /admin/sensitive-words/import` | 返回成功、重复、失败数量 | +| 文件导入 | 上传文件导入 | `POST /admin/sensitive-words/import` | 支持 `.txt`、`.csv`、`.xlsx` | +| 批量导出 | 导出当前筛选词库 | `GET /admin/sensitive-words/export` | 导出 CSV/XLSX | +| 编辑 | 编辑词条 | `PUT /admin/sensitive-words/{id}` | 修改后刷新敏感词缓存 | +| 删除 | 删除词条 | `DELETE /admin/sensitive-words/{id}` | 需二次确认,建议软删除 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| ID | `id` | 敏感词编号 | 数字 | +| 敏感词 | `word` | 待匹配词条 | 必填,建议唯一 | +| 等级 | `level` | 风险等级 | 低/中/高 | +| 替换词 | `replace_word` | 命中替代文本 | 空值显示 `--` | +| 状态 | `status` | 是否生效 | 启用/停用 | +| 创建时间 | `created_at` | 创建时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 敏感词等级:`low` 低、`medium` 中、`high` 高。 +- 敏感词状态:`active` 启用、`disabled` 停用。 +- 处理动作建议扩展:`replace` 替换、`block` 阻断、`alert` 告警。 +- 当前页面查询结果为空,需支持“暂无数据”空态。 + +截图: + +![敏感词库](screenshots-queried/09-sensitive-words.png) + +### 3.10 文件管理 + +路由:`/file-records` +接口: + +- 复用 `GET /admin/messages/search` +- 文件删除可复用消息/群文件删除接口 + +查询条件: + +- 文件类型:全部、图片、视频、音频、文档、其他 +- 发送人用户名 +- 文件名 +- 开始时间 +- 结束时间 + +表格列: + +- 发送人 +- 接收方 +- 类型 +- 内容 +- 文件名 +- 文件大小 +- URL +- 状态 +- 发送时间 +- 操作 + +行操作: + +- 预览 +- 下载 +- 删除 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按文件条件检索 | `GET /admin/messages/search` | 仅筛选包含文件 URL 的消息 | +| 重 置 | 清空筛选条件 | `GET /admin/messages/search` | 清空类型、发送人、文件名、时间 | +| 刷 新 | 保持当前条件刷新 | `GET /admin/messages/search` | 不改变当前页 | +| 预览 | 查看图片/视频/音频 | 文件 URL | 只对可预览类型显示 | +| 下载 | 下载文件 | 文件 URL | URL 需鉴权或短期签名,避免泄露 | +| 删 除 | 删除文件记录/隐藏文件消息 | 消息或群文件删除接口 | 需二次确认;OSS 文件是否物理删除需配置 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 发送人 | `sender_nickname`、`sender_username` | 文件发送方 | 两行展示 | +| 接收方 | `receiver_name` / `group_name` | 单聊接收人或群 | 两行展示名称和编号 | +| 类型 | `file_type` / `message_type` | 文件类型 | 图片/视频/音频/文档/其他 | +| 内容 | `content` | 文件消息摘要 | 显示预览、下载快捷入口 | +| 文件名 | `file_name` | 原始文件名 | 空值显示 `--` | +| 文件大小 | `file_size` | 文件字节数 | 格式化为 B/KB/MB/GB | +| URL | `file_url` | 文件地址 | 表格省略展示,详情里显示完整 URL | +| 状态 | `status` | 文件消息状态 | 正常/已删除 | +| 发送时间 | `sent_at` | 上传或发送时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 文件分类:图片、视频、音频、文档、其他。 +- 图片扩展名:`jpg`、`jpeg`、`png`、`gif`、`bmp`、`webp`、`svg`。 +- 视频扩展名:`mp4`、`avi`、`mov`、`mkv`、`wmv`、`flv`、`webm`。 +- 音频扩展名:`mp3`、`wav`、`flac`、`aac`、`ogg`、`wma`。 +- 文档扩展名:`doc`、`docx`、`xls`、`xlsx`、`csv`、`ppt`、`pptx`、`pdf`、`txt`、`md`。 + +截图: + +![文件管理](screenshots-queried/10-files.png) + +### 3.11 用户反馈 + +路由:`/feedbacks` +核心接口: + +- `GET /admin/feedbacks` +- `PUT /admin/feedbacks/{id}` +- `POST /admin/feedbacks/{id}/priority` +- `POST /admin/feedbacks/batch-handle` +- `GET /admin/feedbacks/export` + +查询条件: + +- 用户 ID +- 状态 +- 优先级 +- 关键词:内容、联系方式 + +顶部操作: + +- 批量处理 +- 导出 CSV + +表格列: + +- 用户 +- 优先级 +- 内容 +- 状态 +- 处理人 +- 创建时间 +- 操作 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查 询 | 按用户、状态、优先级、关键词筛选 | `GET /admin/feedbacks` | 关键词匹配内容或联系方式 | +| 重 置 | 清空筛选条件 | `GET /admin/feedbacks` | 回到全部列表 | +| 批量处理 | 批量更新反馈处理状态 | `POST /admin/feedbacks/batch-handle` | 对选中反馈统一设置状态和处理备注 | +| 导出 CSV | 导出当前筛选结果 | `GET /admin/feedbacks/export` | 包含用户、联系方式、内容、状态、处理信息 | +| 处理 | 单条处理反馈 | `PUT /admin/feedbacks/{id}` | 填写处理状态、处理备注,记录处理人和处理时间 | +| 调整优先级 | 快速修改优先级 | `POST /admin/feedbacks/{id}/priority` | 修改后刷新列表 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 用户 | `user_id`、`username`、`nickname` | 反馈用户 | 支持按用户 ID 筛选 | +| 优先级 | `priority` | 处理优先级 | 低/中/高,颜色区分 | +| 内容 | `content` | 反馈正文 | 长文本省略,详情查看完整 | +| 状态 | `status` | 处理状态 | 待处理/处理中/已处理/已关闭 | +| 处理人 | `handler_name` | 最后处理管理员 | 未处理显示 `--` | +| 创建时间 | `created_at` | 反馈提交时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 优先级:`low` 低、`medium` 中、`high` 高。 +- 反馈状态:`pending` 待处理、`processing` 处理中、`resolved` 已处理、`closed` 已关闭。 +- 当前页面查询结果为空,需支持“暂无数据”空态。 + +截图: + +![用户反馈](screenshots-queried/11-feedback.png) + +### 3.12 操作日志 + +路由:`/audit-logs` +接口:`GET /admin/audit/logs` + +查询条件: + +- 动作 +- 目标类型 +- 目标用户 +- 目标群 +- 开始时间 +- 结束时间 + +顶部操作: + +- 查询 +- 重置 +- 导出 CSV + +表格列: + +- 管理员 +- 动作 +- 目标类型 +- 目标 +- 查询条件 +- IP +- 时间 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查询 | 按动作、目标类型、目标用户、目标群、时间范围筛选 | `GET /admin/audit/logs` | 查询后页码回到第 1 页 | +| 重 置 | 清空筛选条件 | `GET /admin/audit/logs` | 回到全部日志 | +| 导出 CSV | 导出当前筛选日志 | 导出接口 | 用于审计留存 | +| 详情 | 查看操作详情 | `GET /admin/audit/logs` 或详情接口 | 展示请求参数、变更内容、管理员信息 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 管理员 | `admin_username` | 执行动作的后台账号 | 样例:heiyue | +| 动作 | `action` | 操作类型 | 创建用户、重置密码、封禁等 | +| 目标类型 | `target_type` | 操作对象类型 | 用户、群、消息、文件、设置 | +| 目标 | `target_summary` | 目标摘要 | 用户显示昵称和用户名 | +| 查询条件 | `query_condition` | 操作时使用的查询条件 | 无则显示 `--` | +| IP | `ip` | 管理员操作 IP | IPv4/IPv6 | +| 时间 | `created_at` | 操作发生时间 | `YYYY-MM-DD HH:mm:ss` | + +枚举说明: + +- 动作:创建用户、重置密码、修改用户名、更新状态、调整分组、强制下线、强制注销、批量封禁、按 IP 封禁、封禁群、解封群、解散群、删除消息、删除文件、保存设置等。 +- 目标类型:`user` 用户、`group` 群、`message` 消息、`file` 文件、`setting` 设置。 +- 查询后样例:`heiyue` 重置用户密码,目标为 `往事随风 / hy17716319`,IP 为 `141.164.107.33`。 + +截图: + +![操作日志](screenshots-queried/12-operation-logs.png) + +### 3.13 用户账号审计 + +路由:`/user-account-audit` +接口:`GET /admin/audit/user-account-logs` + +查询条件: + +- 用户编号 +- 用户名 +- 操作者 +- 操作 +- IP +- 时间范围 + +操作类型包括: + +- 修改昵称、修改头像、修改性别、修改签名、修改生日、修改密码 +- 主动注销 +- 管理员修改用户名、管理员更新状态、管理员重置密码、管理员更新安全权限 +- 管理员强制下线、管理员强制注销、管理员调整分组、管理员批量封禁、管理员按 IP 封禁 + +表格列: + +- 操作时间 +- 目标用户 +- 操作者 +- 操作类型 +- 变更摘要 +- IP / 归属地 +- 设备/客户端 + +按钮功能说明: + +- 查询:按用户编号、用户名、操作者、操作、IP、时间范围筛选用户账号审计日志。 +- 重置:清空全部筛选条件并重新查询。 +- 本页不提供写操作,只用于审计追踪。 + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 操作时间 | `created_at` | 账号变更时间 | 倒序排列 | +| 目标用户 | `target_username`、`target_user_no` | 被操作用户 | 两行展示用户名和编号 | +| 操作者 | `actor_type`、`actor_username` | 管理员或用户本人 | 标签区分管理员/用户本人 | +| 操作类型 | `action` | 账号审计动作 | 字典化为中文 | +| 变更摘要 | `details` / `before_data` / `after_data` | 变更内容 | 长内容 tooltip 展示 | +| IP / 归属地 | `ip`、`ip_location` | 操作来源 | IP 和归属地分行展示 | +| 设备/客户端 | `device_id` / `user_agent` | 客户端信息 | 长文本省略,tooltip 查看 | + +操作类型枚举: + +| 值 | 中文 | +| --- | --- | +| `change_nickname` | 修改昵称 | +| `change_avatar` | 修改头像 | +| `change_gender` | 修改性别 | +| `change_signature` | 修改签名 | +| `change_birthday` | 修改生日 | +| `change_password` | 修改密码 | +| `deactivate_account` | 主动注销 | +| `update_username` | 管理员修改用户名 | +| `update_status` | 管理员更新状态 | +| `reset_password` | 管理员重置密码 | +| `update_security_perm` | 管理员更新安全权限 | +| `kick_user` | 管理员强制下线 | +| `force_deactivate` | 管理员强制注销 | +| `assign_policy_group` | 管理员调整分组 | +| `batch_ban_users` | 管理员批量封禁 | +| `ban_users_by_ip` | 管理员按 IP 封禁 | + +截图: + +![用户账号审计](screenshots-queried/13-user-account-audit.png) + +### 3.14 群事件审计 + +路由:`/group-events` +接口:`GET /admin/groups/events` + +查询条件: + +- 群号 +- 操作人 +- 目标用户 +- 事件类型 +- 开始时间 +- 结束时间 + +表格列: + +- 群名称 +- 事件类型 +- 操作人 +- 目标用户 +- 可见范围 +- 旧值 +- 新值 +- 时间 + +按钮功能说明: + +- 查询:按群号、操作人、目标用户、事件类型、时间范围筛选。 +- 重置:清空筛选项重新查询。 +- 本页不提供写操作,数据由群创建、成员加入、踢出、禁言、解散等业务自动写入。 + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 群名称 | `group_name`、`group_no` | 事件所属群 | 两行展示群名和群号 | +| 事件类型 | `event_type` | 群事件 | 成员加入、群创建等 | +| 操作人 | `operator_username`、`operator_nickname` | 发起事件用户 | 系统事件显示系统 | +| 目标用户 | `target_username`、`target_nickname` | 被操作用户 | 无目标显示 `--` | +| 可见范围 | `visibility` | 事件对谁可见 | 全员可见、管理员可见、仅自己 | +| 旧值 | `old_value` | 变更前值 | 无则 `--` | +| 新值 | `new_value` | 变更后值 | 无则 `--` | +| 时间 | `created_at` | 事件时间 | `YYYY-MM-DD HH:mm:ss` | + +事件类型枚举建议: + +- `group_created` 群创建。 +- `member_joined` 成员加入。 +- `member_left` 成员退出。 +- `member_kicked` 成员被踢。 +- `member_muted` 成员禁言。 +- `member_unmuted` 解除禁言。 +- `owner_transferred` 群主转让。 +- `admin_added` 设置管理员。 +- `admin_removed` 取消管理员。 +- `group_name_changed` 修改群名。 +- `group_banned` 群封禁。 +- `group_unbanned` 群解封。 +- `group_dissolved` 群解散。 + +截图: + +![群事件审计](screenshots-queried/14-group-event-audit.png) + +### 3.15 管理员登录历史 + +路由:`/admin-login-history` +接口:`GET /admin/admin-login-history` + +查询条件: + +- 开始时间 +- 结束时间 + +表格列: + +- 用户名 +- 登录时间 +- User-Agent +- IP 地址 + +按钮功能说明: + +- 查询:按开始时间、结束时间筛选管理员登录历史。 +- 分页:默认 `20 条/页`。 +- 本页不提供写操作。 + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 用户名 | `username` | 管理员账号 | 样例:heiyue | +| 登录时间 | `login_at` | 登录成功时间 | 倒序排列 | +| User-Agent | `user_agent` | 浏览器 UA | 长文本横向滚动或 tooltip | +| IP 地址 | `ip` | 登录 IP | IPv4/IPv6 | + +截图: + +![管理员登录](screenshots-queried/15-admin-login.png) + +### 3.16 用户登录历史 + +路由:`/user-login-history` +接口:`GET /admin/user-login-history` + +查询条件: + +- 用户编号 +- 用户名 +- 开始时间 +- 结束时间 +- IP + +表格列: + +- 用户编号 +- 用户名 +- 当前状态 +- 登录时间 +- 设备类型 +- 设备型号 +- 系统版本 +- App 版本 +- IP 地址 +- 归属地 +- 操作 + +行操作: + +- 快速封禁 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查询 | 按用户编号、用户名、时间、IP 筛选 | `GET /admin/user-login-history` | 查询后页码回到第 1 页 | +| 重 置 | 清空筛选条件 | `GET /admin/user-login-history` | 回到全部登录历史 | +| 快速封禁 | 从登录记录封禁用户 | `PATCH /admin/users/{id}/status` | 需二次确认;封禁后撤销在线会话 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 用户编号 | `user_no` | 用户对外编号 | 支持筛选 | +| 用户名 | `username` | 用户名 | 支持筛选 | +| 当前状态 | `status` | 用户当前状态 | 正常/已封禁/已注销 | +| 登录时间 | `login_at` | 本次登录时间 | 倒序排列 | +| 设备类型 | `device_type` | 登录终端 | web/android/ios | +| 设备型号 | `device_model` | 设备名称 | 样例:OPPO PCHM10 | +| 系统版本 | `os_version` | 操作系统版本 | 样例:Android 11 | +| App 版本 | `app_version` | 客户端版本 | web 可显示 `-` | +| IP 地址 | `ip` | 登录 IP | 支持筛选 | +| 归属地 | `ip_location` | IP 地理位置 | 无法解析显示未知 | + +枚举说明: + +- 设备类型:`web` Web 端、`android` Android APP、`ios` iOS APP、`unknown` 未知设备。 +- 用户状态同用户管理。 + +截图: + +![用户登录](screenshots-queried/16-user-login.png) + +### 3.17 IP 账号统计 + +路由:`/ip-user-stats` +核心接口: + +- `GET /admin/audit/ip-user-stats` +- `GET /admin/audit/ip-user-stats/users` +- `POST /admin/users/ban-by-ip` + +查询条件: + +- IP,支持 IPv4 或 IPv6 + +表格列: + +- IP 地址 +- 归属地 +- 关联用户 +- 注册用户 +- 登录用户 +- 正常 +- 已封禁 +- 最近活动 +- 操作 + +行操作: + +- 用户列表 +- 封禁 IP,平台级账号可见 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查询 | 按 IP 筛选统计 | `GET /admin/audit/ip-user-stats` | 支持 IPv4/IPv6 | +| 重 置 | 清空 IP | `GET /admin/audit/ip-user-stats` | 回到全部统计 | +| 用户列表 | 查看该 IP 关联用户 | `GET /admin/audit/ip-user-stats/users` | 弹窗展示注册/登录命中的用户 | +| 封禁 IP | 封禁该 IP 关联用户 | `POST /admin/users/ban-by-ip` | 平台账号可见;返回命中数、成功数、失败数 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| IP 地址 | `ip` | 统计 IP | IPv4/IPv6 | +| 归属地 | `ip_location` | IP 地理位置 | 样例:浙江 绍兴 | +| 关联用户 | `user_count` | 注册或登录命中过该 IP 的用户数 | 大于 1 显示“多账号”标签 | +| 注册用户 | `register_user_count` | 使用该 IP 注册的用户数 | 数字 | +| 登录用户 | `login_user_count` | 使用该 IP 登录的用户数 | 数字 | +| 正常 | `active_user_count` | 关联用户中正常数量 | 数字 | +| 已封禁 | `disabled_user_count` | 关联用户中封禁数量 | 数字 | +| 最近活动 | `last_seen_at` | 最近注册或登录时间 | `YYYY-MM-DD HH:mm:ss` | + +关联用户弹窗字段: + +- 用户 ID,平台账号可见。 +- 用户编号、用户名、昵称。 +- 来源:注册、登录,可同时命中。 +- 状态:正常、已封禁。 +- 最近活动。 + +截图: + +![IP 账号统计](screenshots-queried/17-ip-account-stats.png) + +### 3.18 登录失败日志 + +路由:`/login-failure-logs` +核心接口: + +- `GET /admin/login-failure-logs` +- `POST /admin/security/unlock` + +查询条件: + +- 类型 +- 登录账号 +- IP +- 开始时间 +- 结束时间 + +表格列: + +- 类型 +- 登录账号 +- 失败原因 +- IP 地址 +- User-Agent +- 时间 +- 操作 + +行操作: + +- 解锁 + +按钮功能说明: + +| 按钮 | 对应功能 | 接口 | 详细说明 | +| --- | --- | --- | --- | +| 查询 | 按类型、账号、IP、时间筛选 | `GET /admin/login-failure-logs` | 查询后页码回到第 1 页 | +| 重 置 | 清空筛选条件 | `GET /admin/login-failure-logs` | 回到全部失败记录 | +| 解锁 | 解除账号或 IP 登录锁定 | `POST /admin/security/unlock` | 需二次确认 | + +表格字段说明: + +| 表格字段 | 接口字段建议 | 含义 | 展示规则 | +| --- | --- | --- | --- | +| 类型 | `type` | 登录失败账号类型 | 用户/管理员 | +| 登录账号 | `account` | 输入的账号 | 手机号、用户名或管理员账号 | +| 失败原因 | `reason` | 失败说明 | 样例:密码错误 | +| IP 地址 | `ip` | 失败来源 IP | 支持筛选 | +| User-Agent | `user_agent` | 客户端 UA | 长文本省略 | +| 时间 | `created_at` | 失败时间 | 倒序排列 | + +枚举说明: + +- 类型:`user` 用户、`admin` 管理员。 +- 失败原因:`password_error` 密码错误、`account_not_found` 账号不存在、`account_disabled` 账号被封禁、`captcha_error` 验证码错误、`ip_blocked` IP 被拦截、`too_many_attempts` 连续失败次数过多。 + +截图: + +![登录失败日志](screenshots-queried/18-login-failures.png) + +### 3.19 私域设置 + +路由:`/tenant-settings` +核心接口: + +- `GET /admin/tenant-settings` +- `PUT /admin/tenant-settings` + +配置项: + +- 消息字符上限:范围 1 到 5000,仅允许调低 +- 图片大小上限:范围 1 到 1000 MB +- 文件大小上限:范围 1 到 1000 MB,默认 50 MB +- 最大撤回消息时间:范围 1 到 1440 分钟;群主/管理员可撤回自己或他人消息,普通用户撤回自己的消息固定为 2 分钟 +- 是否开启手机验证码:关闭后,注册仅支持邀请码,短信登录与忘记密码不可用 + +操作: + +- 保存设置 + +按钮与字段说明: + +| 字段/按钮 | 接口字段建议 | 范围/枚举 | 业务影响 | +| --- | --- | --- | --- | +| 消息字符上限 | `message_char_limit` | 1 到 5000,仅允许调低 | 用户发送文本消息时校验长度 | +| 图片大小上限(MB) | `image_size_limit_mb` | 1 到 1000 | 图片上传前后端双校验 | +| 文件大小上限(MB) | `file_size_limit_mb` | 1 到 1000,默认 50 | 文件上传前后端双校验 | +| 最大撤回消息时间(分钟) | `max_recall_minutes` | 1 到 1440 | 群主/管理员撤回自己或他人消息的最长时限 | +| 是否开启手机验证码 | `sms_code_enabled` | 开启/关闭 | 关闭后注册仅支持邀请码,短信登录与忘记密码不可用 | +| 保存设置 | - | - | 提交当前租户配置,后端校验后清理租户配置缓存 | + +枚举说明: + +- `sms_code_enabled=true`:开启手机验证码,支持短信登录、忘记密码、手机号验证码注册。 +- `sms_code_enabled=false`:关闭手机验证码,仅支持邀请码注册。 + +截图: + +![私域设置](screenshots-queried/19-private-domain-settings.png) + +### 3.20 安全策略 + +路由:`/security-settings` +核心接口: + +- `GET /admin/security-settings` +- `PUT /admin/security-settings` +- `POST /admin/security/unlock` + +配置分组与字段: + +- 用户安全 + - 验证码触发次数:连续密码错误该次数后要求验证码,范围 1 到 10 + - 密码最小位数:范围 6 到 32 + - 强制包含字母 +- 注册登录管控 + - 用户注册功能 + - 修改密码功能 + - 用户登录功能 + - Web 端多端登录 +- 聊天消息管控 + - 全局好友功能 + - 全局群消息 + - 全局私聊消息 + - 群聊发言间隔:0 表示不限制;1 到 1000 表示同一群内两次发言的最小间隔 +- 群组功能管控 + - 群号搜索功能 + - 群创建功能 + - 群解散功能 + - 群管理员功能 + - 是否显示群人数 +- 其他功能开关 + - 允许用户注销账号 +- IP 访问控制 + - 注册 IP 黑名单:每行一个 IPv4、IPv6 或 CIDR,命中后无法注册及获取注册短信验证码 + - 登录 IP 黑名单:命中后无法密码登录、短信登录及刷新 Token;黑名单优先级高于用户分组登录白名单 +- 高级功能 + - 消息已读状态 + +操作: + +- 保存设置 + +按钮与字段说明: + +| 分组 | 字段 | 接口字段建议 | 范围/枚举 | 业务影响 | +| --- | --- | --- | --- | --- | +| 用户安全 | 验证码触发次数 | `captcha_trigger_count` | 1 到 10 | 连续密码错误达到次数后要求验证码 | +| 用户安全 | 密码最小位数 | `password_min_length` | 6 到 32 | 注册、修改密码、后台重置密码均生效 | +| 用户安全 | 强制包含字母 | `password_require_letter` | 开启/关闭 | 开启后密码必须包含英文字母 | +| 注册登录管控 | 用户注册功能 | `user_register_enabled` | 开启/关闭 | 关闭后用户端隐藏注册入口 | +| 注册登录管控 | 修改密码功能 | `password_change_enabled` | 开启/关闭 | 关闭后用户无法自行修改密码 | +| 注册登录管控 | 用户登录功能 | `user_login_enabled` | 开启/关闭 | 关闭后用户无法登录,不影响已登录用户 | +| 注册登录管控 | Web 端多端登录 | `web_multi_login_enabled` | 开启/关闭 | 开启后同账号可多浏览器同时登录 | +| 聊天消息管控 | 全局好友功能 | `global_friend_enabled` | 开启/关闭 | 关闭后无法搜索和添加好友 | +| 聊天消息管控 | 全局群消息 | `global_group_message_enabled` | 开启/关闭 | 关闭后所有群无法发送消息 | +| 聊天消息管控 | 全局私聊消息 | `global_private_message_enabled` | 开启/关闭 | 关闭后所有私聊无法发送消息 | +| 聊天消息管控 | 群聊发言间隔(秒) | `group_message_interval_seconds` | 0 到 1000 | 0 不限制,其他值限制同群发言间隔 | +| 群组功能管控 | 群号搜索功能 | `group_search_enabled` | 开启/关闭 | 关闭后隐藏群号和群搜索入口 | +| 群组功能管控 | 群创建功能 | `group_create_enabled` | 开启/关闭 | 关闭后所有人无法新建群聊 | +| 群组功能管控 | 群解散功能 | `group_dissolve_enabled` | 开启/关闭 | 关闭后用户端无法解散群,不影响后台 | +| 群组功能管控 | 群管理员功能 | `group_admin_enabled` | 开启/关闭 | 关闭后无法设置群管理员 | +| 群组功能管控 | 是否显示群人数 | `show_group_member_count` | 开启/关闭 | 关闭后用户端群资料不显示总人数 | +| 其他功能开关 | 允许用户注销账号 | `account_deactivate_enabled` | 开启/关闭 | 关闭后隐藏注销账号按钮 | +| IP 访问控制 | 注册 IP 黑名单 | `register_ip_blacklist` | 多行 IP/CIDR | 命中后无法注册和获取注册短信验证码 | +| IP 访问控制 | 登录 IP 黑名单 | `login_ip_blacklist` | 多行 IP/CIDR | 命中后无法密码登录、短信登录和刷新 Token | +| 高级功能 | 消息已读状态 | `message_read_status_enabled` | 开启/关闭 | 开启后显示消息已读/未读标记 | +| 保存设置 | - | - | 保存安全配置,写操作日志,刷新配置缓存 | + +枚举说明: + +- 所有开关均使用布尔值:`true` 开启,`false` 关闭。 +- IP 黑名单每行一个 IPv4、IPv6 或 CIDR,保存时去空行、去重、校验格式。 +- 登录 IP 黑名单优先于用户分组白名单。 + +截图: + +![安全策略](screenshots-queried/20-security-policy.png) + +## 4. 接口清单 + +从前端静态资源反查到的接口如下,建议后端按 REST 风格补齐: + +| 模块 | 接口 | +| --- | --- | +| 认证 | `/admin/auth/login`, `/admin/auth/profile`, `/admin/auth/password`, `/admin/auth/totp/status`, `/admin/auth/totp/setup`, `/admin/auth/totp/bind`, `/admin/auth/totp/unbind` | +| 统计 | `/admin/stats` | +| 用户 | `/admin/users`, `/admin/users/{id}`, `/admin/users/{id}/status`, `/admin/users/{id}/username`, `/admin/users/{id}/reset_password`, `/admin/users/{id}/ip-whitelist`, `/admin/users/{id}/friend-privilege`, `/admin/users/{id}/security-perm`, `/admin/users/{id}/kick`, `/admin/users/{id}/deactivate`, `/admin/users/batch`, `/admin/users/batch-ban`, `/admin/users/ban-by-ip`, `/admin/users/import-xlsx` | +| 用户分组 | `/admin/user-policy-groups`, `/admin/user-policy-groups/{id}`, `/admin/user-policy-groups/{id}/users`, `/admin/user-policy-groups/ip-whitelist` | +| 群组 | `/admin/groups`, `/admin/groups/{id}`, `/admin/groups/{id}/ban`, `/admin/groups/{id}/unban`, `/admin/groups/{id}/members`, `/admin/groups/{id}/members/kick`, `/admin/groups/{id}/members/mute`, `/admin/groups/{id}/members/unmute`, `/admin/groups/{id}/files`, `/admin/groups/{id}/files/{fileId}`, `/admin/groups/events`, `/admin/groups/quick-create` | +| 消息与文件 | `/admin/messages/search`, `/admin/messages/{id}` | +| 邀请码 | `/admin/invite-codes`, `/admin/invite-codes/{id}`, `/admin/invite-codes/{id}/usages` | +| 敏感词 | `/admin/sensitive-words`, `/admin/sensitive-words/{id}`, `/admin/sensitive-words/import`, `/admin/sensitive-words/export` | +| 反馈 | `/admin/feedbacks`, `/admin/feedbacks/{id}`, `/admin/feedbacks/{id}/priority`, `/admin/feedbacks/batch-handle`, `/admin/feedbacks/export` | +| 审计 | `/admin/audit/logs`, `/admin/audit/user-account-logs`, `/admin/audit/ip-user-stats`, `/admin/audit/ip-user-stats/users`, `/admin/control/audit-logs` | +| 登录日志 | `/admin/admin-login-history`, `/admin/user-login-history`, `/admin/login-failure-logs` | +| 设置 | `/admin/tenant-settings`, `/admin/security-settings`, `/admin/security/unlock` | +| 当前菜单未展示但 bundle 存在 | `/admin/tenants`, `/admin/admins`, `/admin/tenant-admins`, `/admin/roles`, `/admin/nodes`, `/admin/system/nodes-status`, `/admin/avatar-pool`, `/admin/avatar-pool/backfill`, `/admin/system-conversations`, `/admin/system-conversations/send`, `/admin/conversation-notices`, `/admin/reports`, `/admin/login-streak/rank`, `/admin/private-msg/rank`, `/admin/user-password`, `/admin/keyword-alerts`, `/admin/alert-keywords`, `/admin/alert-bot-config`, `/admin/domain/encrypt`, `/admin/domain/decrypt`, `/admin/domain/obfuscate`, `/admin/domain/config`, `/admin/domain/push-update`, `/admin/domain/push-bucket-update` | + +通用响应结构建议: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "items": [], + "total": 0, + "page": 1, + "size": 20 + } +} +``` + +列表接口统一分页参数: + +- `page` +- `page_size` +- 各页面查询条件 + +## 5. 核心数据模型建议 + +建议至少设计以下数据表或领域模型: + +- 管理员账号:账号、密码、TOTP、角色、权限、登录历史 +- 用户:用户编号、用户名、昵称、手机号、头像、状态、注册方式、用户分组、最近登录、创建时间 +- 用户分组:权限开关、好友上限、登录白名单、排序权重、默认分组标记 +- 群组:群号、名称、群主、成员数、状态、创建时间 +- 群成员:用户、群、角色、群昵称、禁言状态、加入时间 +- 消息:会话类型、发送人、接收人/群、消息类型、内容、文件信息、状态、发送时间 +- 文件记录:发送人、接收方、文件类型、文件名、大小、URL、消息关联、状态 +- 邀请码:邀请码、指定用户、自动加好友、使用人数、创建人、状态、有效期 +- 敏感词:词条、等级、替换词、状态、创建时间 +- 用户反馈:用户、联系方式、内容、优先级、状态、处理人、处理备注 +- 操作日志:管理员、动作、目标类型、目标、查询条件、IP、时间 +- 用户账号审计:目标用户、操作者、操作类型、变更前后、IP、归属地、设备 +- 群事件审计:群、事件类型、操作人、目标用户、可见范围、旧值、新值、时间 +- 登录历史:账号类型、账号、登录时间、设备信息、IP、归属地 +- 登录失败日志:类型、账号、失败原因、IP、User-Agent、锁定/解锁状态 +- 租户设置:消息、文件、验证码、撤回时间等私域配置 +- 安全策略:密码、注册登录、聊天、群组、注销、IP 黑名单、已读状态配置 + +## 6. 权限建议 + +前端 bundle 中能看到的权限点包括: + +- `tenant_manage` +- `user_manage` +- `group_manage` +- `view_private_chat` +- `view_group_chat` +- `view_audit_log` + +建议在 HotGo 中拆成更细的菜单/按钮权限: + +- 用户管理:查看、创建、批量创建、导入、详情、重置密码、调整分组、封禁、强制下线、注销、IP 白名单 +- 群组管理:查看、详情、群文件、封禁、解散、成员踢出、禁言/解除禁言 +- 内容审核:消息查看、文件预览下载删除、敏感词增删改导入导出、反馈处理导出 +- 日志审计:查看、导出、解锁、IP 关联用户查看、按 IP 封禁 +- 系统设置:私域设置保存、安全策略保存 + +## 7. 实现优先级 + +第一阶段建议先完成可见菜单: + +1. 后台登录、权限、菜单、布局。 +2. 数据概览统计。 +3. 用户管理、用户分组、群组管理、群成员管理、邀请码管理。 +4. 单聊/群聊消息、文件管理、敏感词库、用户反馈。 +5. 操作日志、用户账号审计、群事件审计、登录历史、IP 统计、失败日志。 +6. 私域设置、安全策略。 + +第二阶段再补 bundle 中存在但当前菜单未展示的能力: + +- 租户、管理员、角色、节点、头像池、系统会话、系统通知、报表、排行榜、关键词告警、域名加密等。 + +## 8. 采集产物 + +本次调研文件保存在: + +- 截图目录:`docs/admin-adfahi-research/screenshots-queried/` +- 每页结构化采集:`docs/admin-adfahi-research/*.json` +- 前端 bundle:`docs/admin-adfahi-research/index-RosMaKu2.js` +- 接口尝试结果:`docs/admin-adfahi-research/api-samples.json` diff --git a/server/api/admin/imdashboard/im_dashboard.go b/server/api/admin/imdashboard/im_dashboard.go new file mode 100644 index 0000000..81bbc59 --- /dev/null +++ b/server/api/admin/imdashboard/im_dashboard.go @@ -0,0 +1,25 @@ +package imdashboard + +import ( + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type OverviewReq struct { + g.Meta `path:"/imDashboard/overview" method:"get" tags:"IM数据概览" summary:"获取IM数据概览"` + sysin.ImDashboardOverviewInp +} + +type OverviewRes struct { + *sysin.ImDashboardOverviewModel +} + +type TrendsReq struct { + g.Meta `path:"/imDashboard/trends" method:"get" tags:"IM数据概览" summary:"获取IM增长趋势"` + sysin.ImDashboardTrendsInp +} + +type TrendsRes struct { + List []*sysin.ImDashboardTrendModel `json:"list" dc:"趋势数据"` +} diff --git a/server/api/admin/imgroupmessage/im_group_message.go b/server/api/admin/imgroupmessage/im_group_message.go new file mode 100644 index 0000000..bda8530 --- /dev/null +++ b/server/api/admin/imgroupmessage/im_group_message.go @@ -0,0 +1,37 @@ +package imgroupmessage + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imGroupMessage/list" method:"get" tags:"IM群聊消息" summary:"获取群聊消息列表"` + sysin.ImGroupMessageListInp +} +type ListRes struct { + form.PageRes + List []*sysin.ImMessageListModel `json:"list" dc:"数据列表"` +} +type ViewReq struct { + g.Meta `path:"/imGroupMessage/view" method:"get" tags:"IM群聊消息" summary:"获取群聊消息详情"` + sysin.ImMessageViewInp +} +type ViewRes struct{ *sysin.ImMessageViewModel } +type HideReq struct { + g.Meta `path:"/imGroupMessage/hide" method:"post" tags:"IM群聊消息" summary:"隐藏群聊消息"` + sysin.ImMessageActionInp +} +type HideRes struct{ *sysin.ImMessageCommandModel } +type DeleteReq struct { + g.Meta `path:"/imGroupMessage/delete" method:"post" tags:"IM群聊消息" summary:"删除群聊消息"` + sysin.ImMessageActionInp +} +type DeleteRes struct{ *sysin.ImMessageCommandModel } +type ExportReq struct { + g.Meta `path:"/imGroupMessage/export" method:"get" tags:"IM群聊消息" summary:"导出群聊消息"` + sysin.ImGroupMessageListInp +} +type ExportRes struct{} diff --git a/server/api/admin/imloginfailurelog/im_login_failure_log.go b/server/api/admin/imloginfailurelog/im_login_failure_log.go new file mode 100644 index 0000000..ce95a03 --- /dev/null +++ b/server/api/admin/imloginfailurelog/im_login_failure_log.go @@ -0,0 +1,37 @@ +package imloginfailurelog + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imLoginFailureLog/list" method:"get" tags:"IM登录失败日志" summary:"获取登录失败日志"` + sysin.ImLoginFailureLogListInp +} +type ListRes struct { + form.PageRes + List []*sysin.ImLoginFailureLogModel `json:"list" dc:"数据列表"` +} +type ViewReq struct { + g.Meta `path:"/imLoginFailureLog/view" method:"get" tags:"IM登录失败日志" summary:"获取登录失败详情"` + sysin.ImLoginFailureViewInp +} +type ViewRes struct{ *sysin.ImLoginFailureLogModel } +type ExportReq struct { + g.Meta `path:"/imLoginFailureLog/export" method:"get" tags:"IM登录失败日志" summary:"导出登录失败日志"` + sysin.ImLoginFailureLogListInp +} +type ExportRes struct{} +type AddBlacklistReq struct { + g.Meta `path:"/imLoginFailureLog/addBlacklist" method:"post" tags:"IM登录失败日志" summary:"将IP加入黑名单"` + sysin.ImLoginFailureAddBlacklistInp +} +type AddBlacklistRes struct{} +type BanUserReq struct { + g.Meta `path:"/imLoginFailureLog/banUser" method:"post" tags:"IM登录失败日志" summary:"封禁关联用户"` + sysin.ImLoginFailureBanUserInp +} +type BanUserRes struct{ *sysin.ImUserCommandModel } diff --git a/server/api/admin/imprivatemessage/im_private_message.go b/server/api/admin/imprivatemessage/im_private_message.go new file mode 100644 index 0000000..f3acd48 --- /dev/null +++ b/server/api/admin/imprivatemessage/im_private_message.go @@ -0,0 +1,37 @@ +package imprivatemessage + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imPrivateMessage/list" method:"get" tags:"IM单聊消息" summary:"获取单聊消息列表"` + sysin.ImPrivateMessageListInp +} +type ListRes struct { + form.PageRes + List []*sysin.ImMessageListModel `json:"list" dc:"数据列表"` +} +type ViewReq struct { + g.Meta `path:"/imPrivateMessage/view" method:"get" tags:"IM单聊消息" summary:"获取单聊消息详情"` + sysin.ImMessageViewInp +} +type ViewRes struct{ *sysin.ImMessageViewModel } +type HideReq struct { + g.Meta `path:"/imPrivateMessage/hide" method:"post" tags:"IM单聊消息" summary:"隐藏单聊消息"` + sysin.ImMessageActionInp +} +type HideRes struct{ *sysin.ImMessageCommandModel } +type DeleteReq struct { + g.Meta `path:"/imPrivateMessage/delete" method:"post" tags:"IM单聊消息" summary:"删除单聊消息"` + sysin.ImMessageActionInp +} +type DeleteRes struct{ *sysin.ImMessageCommandModel } +type ExportReq struct { + g.Meta `path:"/imPrivateMessage/export" method:"get" tags:"IM单聊消息" summary:"导出单聊消息"` + sysin.ImPrivateMessageListInp +} +type ExportRes struct{} diff --git a/server/api/admin/imsecuritypolicy/im_security_policy.go b/server/api/admin/imsecuritypolicy/im_security_policy.go new file mode 100644 index 0000000..5042a88 --- /dev/null +++ b/server/api/admin/imsecuritypolicy/im_security_policy.go @@ -0,0 +1,59 @@ +package imsecuritypolicy + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imSecurityPolicy/list" method:"get" tags:"IM安全策略" summary:"获取安全策略历史"` + sysin.ImSecurityPolicyListInp +} +type ListRes struct { + form.PageRes + List []*sysin.ImSecurityPolicyModel `json:"list"` +} +type ViewReq struct { + g.Meta `path:"/imSecurityPolicy/view" method:"get" tags:"IM安全策略" summary:"获取安全策略详情"` + sysin.ImSecurityPolicyViewInp +} +type ViewRes struct{ *sysin.ImSecurityPolicyModel } +type SaveReq struct { + g.Meta `path:"/imSecurityPolicy/save" method:"post" tags:"IM安全策略" summary:"保存安全策略草稿"` + sysin.ImSecurityPolicySaveInp +} +type SaveRes struct { + Id int64 `json:"id"` +} +type PublishReq struct { + g.Meta `path:"/imSecurityPolicy/publish" method:"post" tags:"IM安全策略" summary:"发布安全策略"` + sysin.ImSecurityPolicyPublishInp +} +type PublishRes struct{} +type RestoreDefaultReq struct { + g.Meta `path:"/imSecurityPolicy/restoreDefault" method:"post" tags:"IM安全策略" summary:"恢复默认安全策略"` + sysin.ImSecurityPolicyRestoreInp +} +type RestoreDefaultRes struct { + Id int64 `json:"id"` +} +type IPRuleListReq struct { + g.Meta `path:"/imSecurityPolicy/ipRuleList" method:"get" tags:"IM安全策略" summary:"获取IP黑白名单"` + sysin.ImSecurityIPRuleListInp +} +type IPRuleListRes struct { + form.PageRes + List []*sysin.ImSecurityIPRuleModel `json:"list"` +} +type IPRuleEditReq struct { + g.Meta `path:"/imSecurityPolicy/ipRuleEdit" method:"post" tags:"IM安全策略" summary:"新增或编辑IP规则"` + sysin.ImSecurityIPRuleEditInp +} +type IPRuleEditRes struct{} +type IPRuleDeleteReq struct { + g.Meta `path:"/imSecurityPolicy/ipRuleDelete" method:"post" tags:"IM安全策略" summary:"删除IP规则"` + sysin.ImSecurityIPRuleDeleteInp +} +type IPRuleDeleteRes struct{} diff --git a/server/api/admin/imsensitiveword/im_sensitive_word.go b/server/api/admin/imsensitiveword/im_sensitive_word.go new file mode 100644 index 0000000..2b993cb --- /dev/null +++ b/server/api/admin/imsensitiveword/im_sensitive_word.go @@ -0,0 +1,51 @@ +package imsensitiveword + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imSensitiveWord/list" method:"get" tags:"IM敏感词" summary:"获取敏感词列表"` + sysin.ImSensitiveWordListInp +} +type ListRes struct { + form.PageRes + List []*sysin.ImSensitiveWordListModel `json:"list"` +} +type EditReq struct { + g.Meta `path:"/imSensitiveWord/edit" method:"post" tags:"IM敏感词" summary:"新增或编辑敏感词"` + sysin.ImSensitiveWordEditInp +} +type EditRes struct{} +type DeleteReq struct { + g.Meta `path:"/imSensitiveWord/delete" method:"post" tags:"IM敏感词" summary:"删除敏感词"` + sysin.ImSensitiveWordDeleteInp +} +type DeleteRes struct{} +type StatusReq struct { + g.Meta `path:"/imSensitiveWord/status" method:"post" tags:"IM敏感词" summary:"更新敏感词状态"` + sysin.ImSensitiveWordStatusInp +} +type StatusRes struct{} +type BatchImportReq struct { + g.Meta `path:"/imSensitiveWord/batchImport" method:"post" tags:"IM敏感词" summary:"批量导入敏感词"` + sysin.ImSensitiveWordBatchImportInp +} +type BatchImportRes struct { + *sysin.ImSensitiveWordBatchImportModel +} +type GroupListReq struct { + g.Meta `path:"/imSensitiveWord/groupList" method:"get" tags:"IM敏感词" summary:"获取敏感词分组"` + sysin.ImSensitiveWordGroupListInp +} +type GroupListRes struct { + List []*sysin.ImSensitiveWordGroupModel `json:"list"` +} +type GroupEditReq struct { + g.Meta `path:"/imSensitiveWord/groupEdit" method:"post" tags:"IM敏感词" summary:"新增或编辑敏感词分组"` + sysin.ImSensitiveWordGroupEditInp +} +type GroupEditRes struct{} diff --git a/server/api/admin/imuserloginhistory/im_user_login_history.go b/server/api/admin/imuserloginhistory/im_user_login_history.go new file mode 100644 index 0000000..3665840 --- /dev/null +++ b/server/api/admin/imuserloginhistory/im_user_login_history.go @@ -0,0 +1,22 @@ +package imuserloginhistory + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imUserLoginHistory/list" method:"get" tags:"IM用户登录历史" summary:"获取用户登录历史"` + sysin.ImUserLoginHistoryListInp +} +type ListRes struct { + form.PageRes + List []*sysin.ImUserLoginHistoryModel `json:"list" dc:"数据列表"` +} +type ExportReq struct { + g.Meta `path:"/imUserLoginHistory/export" method:"get" tags:"IM用户登录历史" summary:"导出用户登录历史"` + sysin.ImUserLoginHistoryListInp +} +type ExportRes struct{} diff --git a/server/api/admin/imuserpolicygroup/im_user_policy_group.go b/server/api/admin/imuserpolicygroup/im_user_policy_group.go new file mode 100644 index 0000000..fc6b0ac --- /dev/null +++ b/server/api/admin/imuserpolicygroup/im_user_policy_group.go @@ -0,0 +1,57 @@ +package imuserpolicygroup + +import ( + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListReq struct { + g.Meta `path:"/imUserPolicyGroup/list" method:"get" tags:"IM用户策略分组" summary:"获取用户策略分组列表"` + sysin.ImUserPolicyGroupListInp +} +type ListRes struct { + form.PageRes + List []*sysin.ImUserPolicyGroupListModel `json:"list"` +} +type ViewReq struct { + g.Meta `path:"/imUserPolicyGroup/view" method:"get" tags:"IM用户策略分组" summary:"获取用户策略分组详情"` + sysin.ImUserPolicyGroupViewInp +} +type ViewRes struct { + *sysin.ImUserPolicyGroupViewModel +} +type EditReq struct { + g.Meta `path:"/imUserPolicyGroup/edit" method:"post" tags:"IM用户策略分组" summary:"新增或编辑用户策略分组"` + sysin.ImUserPolicyGroupEditInp +} +type EditRes struct{} +type DeleteReq struct { + g.Meta `path:"/imUserPolicyGroup/delete" method:"post" tags:"IM用户策略分组" summary:"删除用户策略分组"` + sysin.ImUserPolicyGroupDeleteInp +} +type DeleteRes struct{} +type StatusReq struct { + g.Meta `path:"/imUserPolicyGroup/status" method:"post" tags:"IM用户策略分组" summary:"更新用户策略分组状态"` + sysin.ImUserPolicyGroupStatusInp +} +type StatusRes struct{} +type MemberListReq struct { + g.Meta `path:"/imUserPolicyGroup/memberList" method:"get" tags:"IM用户策略分组" summary:"获取策略分组成员"` + sysin.ImUserPolicyGroupMemberListInp +} +type MemberListRes struct { + form.PageRes + List []*sysin.ImUserPolicyGroupMemberModel `json:"list"` +} +type MemberAddReq struct { + g.Meta `path:"/imUserPolicyGroup/memberAdd" method:"post" tags:"IM用户策略分组" summary:"添加策略分组成员"` + sysin.ImUserPolicyGroupMemberAddInp +} +type MemberAddRes struct{} +type MemberRemoveReq struct { + g.Meta `path:"/imUserPolicyGroup/memberRemove" method:"post" tags:"IM用户策略分组" summary:"移除策略分组成员"` + sysin.ImUserPolicyGroupMemberRemoveInp +} +type MemberRemoveRes struct{} diff --git a/server/internal/consts/im_content_audit.go b/server/internal/consts/im_content_audit.go new file mode 100644 index 0000000..7ad3e2d --- /dev/null +++ b/server/internal/consts/im_content_audit.go @@ -0,0 +1,84 @@ +package consts + +import ( + "hotgo/internal/library/dict" + "hotgo/internal/model" +) + +const ( + ImMessageContentText = "text" + ImMessageContentImage = "image" + ImMessageContentVideo = "video" + ImMessageContentAudio = "audio" + ImMessageContentVoice = "voice" + ImMessageContentFile = "file" + ImMessageContentCard = "card" + ImMessageContentSystem = "system" + + ImMessageStatusNormal = "normal" + ImMessageStatusDeleted = "deleted" + ImMessageStatusRecalled = "recalled" + ImMessageStatusBlocked = "blocked" + ImMessageStatusHidden = "hidden" +) + +var ImMessageContentTypeOptions = []*model.Option{ + dict.GenInfoOption(ImMessageContentText, "文本"), + dict.GenPrimaryOption(ImMessageContentImage, "图片"), + dict.GenPrimaryOption(ImMessageContentVideo, "视频"), + dict.GenPrimaryOption(ImMessageContentAudio, "语音"), + dict.GenPrimaryOption(ImMessageContentVoice, "语音"), + dict.GenDefaultOption(ImMessageContentFile, "文件"), + dict.GenDefaultOption(ImMessageContentCard, "名片"), + dict.GenWarningOption(ImMessageContentSystem, "系统"), +} + +var ImMessageStatusOptions = []*model.Option{ + dict.GenSuccessOption(ImMessageStatusNormal, "正常"), + dict.GenErrorOption(ImMessageStatusDeleted, "后台删除"), + dict.GenWarningOption(ImMessageStatusRecalled, "用户撤回"), + dict.GenErrorOption(ImMessageStatusBlocked, "风控拦截"), + dict.GenWarningOption(ImMessageStatusHidden, "已隐藏"), +} + +var ImLoginTypeOptions = []*model.Option{ + dict.GenPrimaryOption("password", "密码"), + dict.GenPrimaryOption("sms_code", "短信验证码"), + dict.GenInfoOption("sso", "单点登录"), + dict.GenInfoOption("qr_code", "二维码"), + dict.GenDefaultOption("token_refresh", "令牌刷新"), +} + +var ImLoginEventTypeOptions = []*model.Option{ + dict.GenSuccessOption("login", "登录"), + dict.GenInfoOption("refresh", "刷新会话"), + dict.GenDefaultOption("logout", "主动退出"), + dict.GenWarningOption("kicked", "被踢下线"), + dict.GenWarningOption("session_expired", "会话过期"), +} + +var ImClientPlatformOptions = []*model.Option{ + dict.GenPrimaryOption("ios", "iOS"), + dict.GenSuccessOption("android", "Android"), + dict.GenInfoOption("web", "Web"), + dict.GenDefaultOption("desktop", "桌面端"), + dict.GenDefaultOption("unknown", "未知"), +} + +var ImLoginFailureTypeOptions = []*model.Option{ + dict.GenErrorOption("password_error", "密码错误"), + dict.GenErrorOption("code_error", "验证码错误"), + dict.GenWarningOption("account_not_found", "账号不存在"), + dict.GenErrorOption("account_disabled", "账号已禁用"), + dict.GenWarningOption("rate_limited", "访问频率受限"), + dict.GenErrorOption("risk_blocked", "风控拦截"), +} + +func init() { + dict.RegisterEnums("ImMessageContentTypeOptions", "IM消息内容类型选项", ImMessageContentTypeOptions) + dict.RegisterEnums("ImMessageStatusOptions", "IM消息状态选项", ImMessageStatusOptions) + dict.RegisterEnums("ImLoginTypeOptions", "IM登录方式选项", ImLoginTypeOptions) + dict.RegisterEnums("ImLoginEventTypeOptions", "IM登录事件选项", ImLoginEventTypeOptions) + dict.RegisterEnums("ImClientPlatformOptions", "IM客户端平台选项", ImClientPlatformOptions) + dict.RegisterEnums("ImLoginFailureTypeOptions", "IM登录失败类型选项", ImLoginFailureTypeOptions) +} diff --git a/server/internal/consts/im_kafka.go b/server/internal/consts/im_kafka.go index 4d26cfa..7622bcf 100644 --- a/server/internal/consts/im_kafka.go +++ b/server/internal/consts/im_kafka.go @@ -47,6 +47,8 @@ const ( ImAdminCommandUserCancel = "user.account.cancel" ImAdminCommandUserDelete = "user.account.delete" ImAdminCommandUserPasswordReset = "user.password.reset" + ImAdminCommandMessageHide = "message.hide" + ImAdminCommandMessageDelete = "message.delete" ImAdminCommandKafkaDeadReplay = "kafka.dead_letter.replay" ImAdminCommandKafkaDeadIgnore = "kafka.dead_letter.ignore" ImAdminCommandKafkaDeadResolve = "kafka.dead_letter.resolve" diff --git a/server/internal/consts/im_policy.go b/server/internal/consts/im_policy.go new file mode 100644 index 0000000..0d2814b --- /dev/null +++ b/server/internal/consts/im_policy.go @@ -0,0 +1,118 @@ +package consts + +import ( + "hotgo/internal/library/dict" + "hotgo/internal/model" +) + +const ( + ImPolicyActionAllow = "allow" + ImPolicyActionDeny = "deny" + ImPolicyActionChallenge = "challenge" + ImPolicyActionAuditOnly = "audit_only" + ImSecurityActionBlock = "block" + + ImSensitiveMatchExact = "exact" + ImSensitiveMatchContains = "contains" + ImSensitiveMatchRegex = "regex" + ImSensitiveMatchPinyin = "pinyin" + + ImSensitiveActionReplace = "replace" + ImSensitiveActionBlock = "block" + ImSensitiveActionAuditOnly = "audit_only" + ImSensitiveActionManualReview = "manual_review" + + ImRiskLevelLow = "low" + ImRiskLevelMedium = "medium" + ImRiskLevelHigh = "high" + + ImSecurityPublishDraft = "draft" + ImSecurityPublishPublished = "published" + ImSecurityPublishArchived = "archived" + ImSecurityPublishFailed = "failed" + + ImCurfewDateDaily = "daily" + ImCurfewDateWeekday = "weekday" + ImCurfewDateWeekend = "weekend" + ImCurfewDateCustom = "custom" + + ImCurfewScopePrivate = "private_message" + ImCurfewScopeGroup = "group_message" +) + +var ( + ImPolicyActionOptions = []*model.Option{ + dict.GenSuccessOption(ImPolicyActionAllow, "放行"), + dict.GenErrorOption(ImPolicyActionDeny, "拦截"), + dict.GenWarningOption(ImPolicyActionChallenge, "二次验证"), + dict.GenInfoOption(ImPolicyActionAuditOnly, "仅审计"), + } + ImSecurityActionOptions = []*model.Option{ + dict.GenSuccessOption(ImPolicyActionAllow, "放行"), + dict.GenWarningOption(ImPolicyActionChallenge, "二次验证"), + dict.GenErrorOption(ImSecurityActionBlock, "阻断"), + dict.GenInfoOption(ImPolicyActionAuditOnly, "仅审计"), + } + ImSensitiveMatchTypeOptions = []*model.Option{ + dict.GenPrimaryOption(ImSensitiveMatchExact, "精确匹配"), + dict.GenDefaultOption(ImSensitiveMatchContains, "包含匹配"), + dict.GenWarningOption(ImSensitiveMatchRegex, "正则匹配"), + dict.GenInfoOption(ImSensitiveMatchPinyin, "拼音匹配"), + } + ImSensitiveActionOptions = []*model.Option{ + dict.GenWarningOption(ImSensitiveActionReplace, "替换"), + dict.GenErrorOption(ImSensitiveActionBlock, "拦截"), + dict.GenInfoOption(ImSensitiveActionAuditOnly, "仅审计"), + dict.GenPrimaryOption(ImSensitiveActionManualReview, "人工审核"), + } + ImRiskLevelOptions = []*model.Option{ + dict.GenSuccessOption(ImRiskLevelLow, "低风险"), + dict.GenWarningOption(ImRiskLevelMedium, "中风险"), + dict.GenErrorOption(ImRiskLevelHigh, "高风险"), + } + ImSecurityPublishStatusOptions = []*model.Option{ + dict.GenWarningOption(ImSecurityPublishDraft, "草稿"), + dict.GenSuccessOption(ImSecurityPublishPublished, "已发布"), + dict.GenDefaultOption(ImSecurityPublishArchived, "已归档"), + dict.GenErrorOption(ImSecurityPublishFailed, "发布失败"), + } + ImCurfewDateTypeOptions = []*model.Option{ + dict.GenDefaultOption(ImCurfewDateDaily, "每天"), + dict.GenPrimaryOption(ImCurfewDateWeekday, "工作日"), + dict.GenInfoOption(ImCurfewDateWeekend, "周末"), + dict.GenWarningOption(ImCurfewDateCustom, "自定义日期"), + } + ImCurfewScopeOptions = []*model.Option{ + dict.GenErrorOption(ImCurfewScopePrivate, "单聊/私聊"), + dict.GenWarningOption(ImCurfewScopeGroup, "群聊"), + } + ImSensitiveSceneOptions = []*model.Option{ + dict.GenPrimaryOption("private_message", "单聊消息"), + dict.GenPrimaryOption("group_message", "群聊消息"), + dict.GenInfoOption("user_profile", "用户资料"), + dict.GenInfoOption("group_profile", "群资料"), + dict.GenWarningOption("feedback", "用户反馈"), + dict.GenWarningOption("report", "举报内容"), + } + ImFriendEntryOptions = []*model.Option{ + dict.GenDefaultOption("search", "用户搜索"), + dict.GenDefaultOption("profile_card", "个人名片"), + dict.GenDefaultOption("group_member", "群成员"), + dict.GenDefaultOption("qrcode", "二维码"), + dict.GenDefaultOption("invite_auto_add", "邀请注册自动添加"), + dict.GenDefaultOption("recommendation", "推荐联系人"), + } +) + +func init() { + dict.RegisterEnums("ImPolicyActionOptions", "IM策略动作", ImPolicyActionOptions) + dict.RegisterEnums("ImSecurityActionOptions", "IM安全策略动作", ImSecurityActionOptions) + dict.RegisterEnums("ImSensitiveMatchTypeOptions", "IM敏感词匹配类型", ImSensitiveMatchTypeOptions) + dict.RegisterEnums("ImSensitiveActionOptions", "IM敏感词动作", ImSensitiveActionOptions) + dict.RegisterEnums("ImRiskLevelOptions", "IM风险等级", ImRiskLevelOptions) + dict.RegisterEnums("ImSecurityPublishStatusOptions", "IM安全策略发布状态", ImSecurityPublishStatusOptions) + dict.RegisterEnums("ImCurfewDateTypeOptions", "IM宵禁日期类型", ImCurfewDateTypeOptions) + dict.RegisterEnums("ImCurfewScopeOptions", "IM宵禁范围", ImCurfewScopeOptions) + dict.RegisterEnums("ImSensitiveSceneOptions", "IM敏感词场景", ImSensitiveSceneOptions) + dict.RegisterEnums("ImFriendEntryOptions", "IM好友入口", ImFriendEntryOptions) +} diff --git a/server/internal/controller/admin/sys/im_dashboard.go b/server/internal/controller/admin/sys/im_dashboard.go new file mode 100644 index 0000000..40bac67 --- /dev/null +++ b/server/internal/controller/admin/sys/im_dashboard.go @@ -0,0 +1,31 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imdashboard" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImDashboard = cImDashboard{} + +type cImDashboard struct{} + +func (c *cImDashboard) Overview(ctx context.Context, req *imdashboard.OverviewReq) (res *imdashboard.OverviewRes, err error) { + data, err := service.SysImDashboard().Overview(ctx, &req.ImDashboardOverviewInp) + if err != nil { + return nil, err + } + return &imdashboard.OverviewRes{ImDashboardOverviewModel: data}, nil +} + +func (c *cImDashboard) Trends(ctx context.Context, req *imdashboard.TrendsReq) (res *imdashboard.TrendsRes, err error) { + list, err := service.SysImDashboard().Trends(ctx, &req.ImDashboardTrendsInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImDashboardTrendModel{} + } + return &imdashboard.TrendsRes{List: list}, nil +} diff --git a/server/internal/controller/admin/sys/im_group_message.go b/server/internal/controller/admin/sys/im_group_message.go new file mode 100644 index 0000000..2e4aa0b --- /dev/null +++ b/server/internal/controller/admin/sys/im_group_message.go @@ -0,0 +1,40 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imgroupmessage" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImGroupMessage = cImGroupMessage{} + +type cImGroupMessage struct{} + +func (c *cImGroupMessage) List(ctx context.Context, req *imgroupmessage.ListReq) (res *imgroupmessage.ListRes, err error) { + list, total, err := service.SysImGroupMessage().List(ctx, &req.ImGroupMessageListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImMessageListModel{} + } + res = &imgroupmessage.ListRes{List: list} + res.PageRes.Pack(req, total) + return +} +func (c *cImGroupMessage) View(ctx context.Context, req *imgroupmessage.ViewReq) (*imgroupmessage.ViewRes, error) { + data, err := service.SysImGroupMessage().View(ctx, &req.ImMessageViewInp) + return &imgroupmessage.ViewRes{ImMessageViewModel: data}, err +} +func (c *cImGroupMessage) Hide(ctx context.Context, req *imgroupmessage.HideReq) (*imgroupmessage.HideRes, error) { + data, err := service.SysImGroupMessage().Hide(ctx, &req.ImMessageActionInp) + return &imgroupmessage.HideRes{ImMessageCommandModel: data}, err +} +func (c *cImGroupMessage) Delete(ctx context.Context, req *imgroupmessage.DeleteReq) (*imgroupmessage.DeleteRes, error) { + data, err := service.SysImGroupMessage().Delete(ctx, &req.ImMessageActionInp) + return &imgroupmessage.DeleteRes{ImMessageCommandModel: data}, err +} +func (c *cImGroupMessage) Export(ctx context.Context, req *imgroupmessage.ExportReq) (*imgroupmessage.ExportRes, error) { + return &imgroupmessage.ExportRes{}, service.SysImGroupMessage().Export(ctx, &req.ImGroupMessageListInp) +} diff --git a/server/internal/controller/admin/sys/im_login_failure_log.go b/server/internal/controller/admin/sys/im_login_failure_log.go new file mode 100644 index 0000000..e6693b1 --- /dev/null +++ b/server/internal/controller/admin/sys/im_login_failure_log.go @@ -0,0 +1,39 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imloginfailurelog" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImLoginFailureLog = cImLoginFailureLog{} + +type cImLoginFailureLog struct{} + +func (c *cImLoginFailureLog) List(ctx context.Context, req *imloginfailurelog.ListReq) (res *imloginfailurelog.ListRes, err error) { + list, total, err := service.SysImLoginFailureLog().List(ctx, &req.ImLoginFailureLogListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImLoginFailureLogModel{} + } + res = &imloginfailurelog.ListRes{List: list} + res.PageRes.Pack(req, total) + return +} +func (c *cImLoginFailureLog) View(ctx context.Context, req *imloginfailurelog.ViewReq) (*imloginfailurelog.ViewRes, error) { + data, err := service.SysImLoginFailureLog().View(ctx, &req.ImLoginFailureViewInp) + return &imloginfailurelog.ViewRes{ImLoginFailureLogModel: data}, err +} +func (c *cImLoginFailureLog) Export(ctx context.Context, req *imloginfailurelog.ExportReq) (*imloginfailurelog.ExportRes, error) { + return &imloginfailurelog.ExportRes{}, service.SysImLoginFailureLog().Export(ctx, &req.ImLoginFailureLogListInp) +} +func (c *cImLoginFailureLog) AddBlacklist(ctx context.Context, req *imloginfailurelog.AddBlacklistReq) (*imloginfailurelog.AddBlacklistRes, error) { + return &imloginfailurelog.AddBlacklistRes{}, service.SysImLoginFailureLog().AddBlacklist(ctx, &req.ImLoginFailureAddBlacklistInp) +} +func (c *cImLoginFailureLog) BanUser(ctx context.Context, req *imloginfailurelog.BanUserReq) (*imloginfailurelog.BanUserRes, error) { + data, err := service.SysImLoginFailureLog().BanUser(ctx, &req.ImLoginFailureBanUserInp) + return &imloginfailurelog.BanUserRes{ImUserCommandModel: data}, err +} diff --git a/server/internal/controller/admin/sys/im_private_message.go b/server/internal/controller/admin/sys/im_private_message.go new file mode 100644 index 0000000..01d2c15 --- /dev/null +++ b/server/internal/controller/admin/sys/im_private_message.go @@ -0,0 +1,40 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imprivatemessage" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImPrivateMessage = cImPrivateMessage{} + +type cImPrivateMessage struct{} + +func (c *cImPrivateMessage) List(ctx context.Context, req *imprivatemessage.ListReq) (res *imprivatemessage.ListRes, err error) { + list, total, err := service.SysImPrivateMessage().List(ctx, &req.ImPrivateMessageListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImMessageListModel{} + } + res = &imprivatemessage.ListRes{List: list} + res.PageRes.Pack(req, total) + return +} +func (c *cImPrivateMessage) View(ctx context.Context, req *imprivatemessage.ViewReq) (*imprivatemessage.ViewRes, error) { + data, err := service.SysImPrivateMessage().View(ctx, &req.ImMessageViewInp) + return &imprivatemessage.ViewRes{ImMessageViewModel: data}, err +} +func (c *cImPrivateMessage) Hide(ctx context.Context, req *imprivatemessage.HideReq) (*imprivatemessage.HideRes, error) { + data, err := service.SysImPrivateMessage().Hide(ctx, &req.ImMessageActionInp) + return &imprivatemessage.HideRes{ImMessageCommandModel: data}, err +} +func (c *cImPrivateMessage) Delete(ctx context.Context, req *imprivatemessage.DeleteReq) (*imprivatemessage.DeleteRes, error) { + data, err := service.SysImPrivateMessage().Delete(ctx, &req.ImMessageActionInp) + return &imprivatemessage.DeleteRes{ImMessageCommandModel: data}, err +} +func (c *cImPrivateMessage) Export(ctx context.Context, req *imprivatemessage.ExportReq) (*imprivatemessage.ExportRes, error) { + return &imprivatemessage.ExportRes{}, service.SysImPrivateMessage().Export(ctx, &req.ImPrivateMessageListInp) +} diff --git a/server/internal/controller/admin/sys/im_security_policy.go b/server/internal/controller/admin/sys/im_security_policy.go new file mode 100644 index 0000000..a1e87cb --- /dev/null +++ b/server/internal/controller/admin/sys/im_security_policy.go @@ -0,0 +1,72 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imsecuritypolicy" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImSecurityPolicy = cImSecurityPolicy{} + +type cImSecurityPolicy struct{} + +func (c *cImSecurityPolicy) List(ctx context.Context, req *imsecuritypolicy.ListReq) (res *imsecuritypolicy.ListRes, err error) { + list, total, err := service.SysImSecurityPolicy().List(ctx, &req.ImSecurityPolicyListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImSecurityPolicyModel{} + } + res = new(imsecuritypolicy.ListRes) + res.List = list + res.PageRes.Pack(req, total) + return +} +func (c *cImSecurityPolicy) View(ctx context.Context, req *imsecuritypolicy.ViewReq) (res *imsecuritypolicy.ViewRes, err error) { + data, err := service.SysImSecurityPolicy().View(ctx, &req.ImSecurityPolicyViewInp) + if err != nil { + return nil, err + } + return &imsecuritypolicy.ViewRes{ImSecurityPolicyModel: data}, nil +} +func (c *cImSecurityPolicy) Save(ctx context.Context, req *imsecuritypolicy.SaveReq) (res *imsecuritypolicy.SaveRes, err error) { + id, err := service.SysImSecurityPolicy().Save(ctx, &req.ImSecurityPolicySaveInp) + if err != nil { + return nil, err + } + return &imsecuritypolicy.SaveRes{Id: id}, nil +} +func (c *cImSecurityPolicy) Publish(ctx context.Context, req *imsecuritypolicy.PublishReq) (res *imsecuritypolicy.PublishRes, err error) { + err = service.SysImSecurityPolicy().Publish(ctx, &req.ImSecurityPolicyPublishInp) + return +} +func (c *cImSecurityPolicy) RestoreDefault(ctx context.Context, req *imsecuritypolicy.RestoreDefaultReq) (res *imsecuritypolicy.RestoreDefaultRes, err error) { + id, err := service.SysImSecurityPolicy().RestoreDefault(ctx, &req.ImSecurityPolicyRestoreInp) + if err != nil { + return nil, err + } + return &imsecuritypolicy.RestoreDefaultRes{Id: id}, nil +} +func (c *cImSecurityPolicy) IPRuleList(ctx context.Context, req *imsecuritypolicy.IPRuleListReq) (res *imsecuritypolicy.IPRuleListRes, err error) { + list, total, err := service.SysImSecurityPolicy().IPRuleList(ctx, &req.ImSecurityIPRuleListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImSecurityIPRuleModel{} + } + res = new(imsecuritypolicy.IPRuleListRes) + res.List = list + res.PageRes.Pack(req, total) + return +} +func (c *cImSecurityPolicy) IPRuleEdit(ctx context.Context, req *imsecuritypolicy.IPRuleEditReq) (res *imsecuritypolicy.IPRuleEditRes, err error) { + err = service.SysImSecurityPolicy().IPRuleEdit(ctx, &req.ImSecurityIPRuleEditInp) + return +} +func (c *cImSecurityPolicy) IPRuleDelete(ctx context.Context, req *imsecuritypolicy.IPRuleDeleteReq) (res *imsecuritypolicy.IPRuleDeleteRes, err error) { + err = service.SysImSecurityPolicy().IPRuleDelete(ctx, &req.ImSecurityIPRuleDeleteInp) + return +} diff --git a/server/internal/controller/admin/sys/im_sensitive_word.go b/server/internal/controller/admin/sys/im_sensitive_word.go new file mode 100644 index 0000000..cee5752 --- /dev/null +++ b/server/internal/controller/admin/sys/im_sensitive_word.go @@ -0,0 +1,59 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imsensitiveword" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImSensitiveWord = cImSensitiveWord{} + +type cImSensitiveWord struct{} + +func (c *cImSensitiveWord) List(ctx context.Context, req *imsensitiveword.ListReq) (res *imsensitiveword.ListRes, err error) { + list, total, err := service.SysImSensitiveWord().List(ctx, &req.ImSensitiveWordListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImSensitiveWordListModel{} + } + res = new(imsensitiveword.ListRes) + res.List = list + res.PageRes.Pack(req, total) + return +} +func (c *cImSensitiveWord) Edit(ctx context.Context, req *imsensitiveword.EditReq) (res *imsensitiveword.EditRes, err error) { + err = service.SysImSensitiveWord().Edit(ctx, &req.ImSensitiveWordEditInp) + return +} +func (c *cImSensitiveWord) Delete(ctx context.Context, req *imsensitiveword.DeleteReq) (res *imsensitiveword.DeleteRes, err error) { + err = service.SysImSensitiveWord().Delete(ctx, &req.ImSensitiveWordDeleteInp) + return +} +func (c *cImSensitiveWord) Status(ctx context.Context, req *imsensitiveword.StatusReq) (res *imsensitiveword.StatusRes, err error) { + err = service.SysImSensitiveWord().Status(ctx, &req.ImSensitiveWordStatusInp) + return +} +func (c *cImSensitiveWord) BatchImport(ctx context.Context, req *imsensitiveword.BatchImportReq) (res *imsensitiveword.BatchImportRes, err error) { + data, err := service.SysImSensitiveWord().BatchImport(ctx, &req.ImSensitiveWordBatchImportInp) + if err != nil { + return nil, err + } + return &imsensitiveword.BatchImportRes{ImSensitiveWordBatchImportModel: data}, nil +} +func (c *cImSensitiveWord) GroupList(ctx context.Context, req *imsensitiveword.GroupListReq) (res *imsensitiveword.GroupListRes, err error) { + list, err := service.SysImSensitiveWord().GroupList(ctx, &req.ImSensitiveWordGroupListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImSensitiveWordGroupModel{} + } + return &imsensitiveword.GroupListRes{List: list}, nil +} +func (c *cImSensitiveWord) GroupEdit(ctx context.Context, req *imsensitiveword.GroupEditReq) (res *imsensitiveword.GroupEditRes, err error) { + err = service.SysImSensitiveWord().GroupEdit(ctx, &req.ImSensitiveWordGroupEditInp) + return +} diff --git a/server/internal/controller/admin/sys/im_user_login_history.go b/server/internal/controller/admin/sys/im_user_login_history.go new file mode 100644 index 0000000..27c4464 --- /dev/null +++ b/server/internal/controller/admin/sys/im_user_login_history.go @@ -0,0 +1,28 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imuserloginhistory" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImUserLoginHistory = cImUserLoginHistory{} + +type cImUserLoginHistory struct{} + +func (c *cImUserLoginHistory) List(ctx context.Context, req *imuserloginhistory.ListReq) (res *imuserloginhistory.ListRes, err error) { + list, total, err := service.SysImUserLoginHistory().List(ctx, &req.ImUserLoginHistoryListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImUserLoginHistoryModel{} + } + res = &imuserloginhistory.ListRes{List: list} + res.PageRes.Pack(req, total) + return +} +func (c *cImUserLoginHistory) Export(ctx context.Context, req *imuserloginhistory.ExportReq) (*imuserloginhistory.ExportRes, error) { + return &imuserloginhistory.ExportRes{}, service.SysImUserLoginHistory().Export(ctx, &req.ImUserLoginHistoryListInp) +} diff --git a/server/internal/controller/admin/sys/im_user_policy_group.go b/server/internal/controller/admin/sys/im_user_policy_group.go new file mode 100644 index 0000000..5e70617 --- /dev/null +++ b/server/internal/controller/admin/sys/im_user_policy_group.go @@ -0,0 +1,66 @@ +package sys + +import ( + "context" + "hotgo/api/admin/imuserpolicygroup" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" +) + +var ImUserPolicyGroup = cImUserPolicyGroup{} + +type cImUserPolicyGroup struct{} + +func (c *cImUserPolicyGroup) List(ctx context.Context, req *imuserpolicygroup.ListReq) (res *imuserpolicygroup.ListRes, err error) { + list, total, err := service.SysImUserPolicyGroup().List(ctx, &req.ImUserPolicyGroupListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImUserPolicyGroupListModel{} + } + res = new(imuserpolicygroup.ListRes) + res.List = list + res.PageRes.Pack(req, total) + return +} +func (c *cImUserPolicyGroup) View(ctx context.Context, req *imuserpolicygroup.ViewReq) (res *imuserpolicygroup.ViewRes, err error) { + data, err := service.SysImUserPolicyGroup().View(ctx, &req.ImUserPolicyGroupViewInp) + if err != nil { + return nil, err + } + return &imuserpolicygroup.ViewRes{ImUserPolicyGroupViewModel: data}, nil +} +func (c *cImUserPolicyGroup) Edit(ctx context.Context, req *imuserpolicygroup.EditReq) (res *imuserpolicygroup.EditRes, err error) { + err = service.SysImUserPolicyGroup().Edit(ctx, &req.ImUserPolicyGroupEditInp) + return +} +func (c *cImUserPolicyGroup) Delete(ctx context.Context, req *imuserpolicygroup.DeleteReq) (res *imuserpolicygroup.DeleteRes, err error) { + err = service.SysImUserPolicyGroup().Delete(ctx, &req.ImUserPolicyGroupDeleteInp) + return +} +func (c *cImUserPolicyGroup) Status(ctx context.Context, req *imuserpolicygroup.StatusReq) (res *imuserpolicygroup.StatusRes, err error) { + err = service.SysImUserPolicyGroup().Status(ctx, &req.ImUserPolicyGroupStatusInp) + return +} +func (c *cImUserPolicyGroup) MemberList(ctx context.Context, req *imuserpolicygroup.MemberListReq) (res *imuserpolicygroup.MemberListRes, err error) { + list, total, err := service.SysImUserPolicyGroup().MemberList(ctx, &req.ImUserPolicyGroupMemberListInp) + if err != nil { + return nil, err + } + if list == nil { + list = []*sysin.ImUserPolicyGroupMemberModel{} + } + res = new(imuserpolicygroup.MemberListRes) + res.List = list + res.PageRes.Pack(req, total) + return +} +func (c *cImUserPolicyGroup) MemberAdd(ctx context.Context, req *imuserpolicygroup.MemberAddReq) (res *imuserpolicygroup.MemberAddRes, err error) { + err = service.SysImUserPolicyGroup().MemberAdd(ctx, &req.ImUserPolicyGroupMemberAddInp) + return +} +func (c *cImUserPolicyGroup) MemberRemove(ctx context.Context, req *imuserpolicygroup.MemberRemoveReq) (res *imuserpolicygroup.MemberRemoveRes, err error) { + err = service.SysImUserPolicyGroup().MemberRemove(ctx, &req.ImUserPolicyGroupMemberRemoveInp) + return +} diff --git a/server/internal/dao/im_message_curfew_rules.go b/server/internal/dao/im_message_curfew_rules.go new file mode 100644 index 0000000..5120dcf --- /dev/null +++ b/server/internal/dao/im_message_curfew_rules.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imMessageCurfewRulesDao is the data access object for the table im_message_curfew_rules. +// You can define custom methods on it to extend its functionality as needed. +type imMessageCurfewRulesDao struct { + *internal.ImMessageCurfewRulesDao +} + +var ( + // ImMessageCurfewRules is a globally accessible object for table im_message_curfew_rules operations. + ImMessageCurfewRules = imMessageCurfewRulesDao{internal.NewImMessageCurfewRulesDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/im_security_ip_rules.go b/server/internal/dao/im_security_ip_rules.go new file mode 100644 index 0000000..9d5a7ff --- /dev/null +++ b/server/internal/dao/im_security_ip_rules.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imSecurityIpRulesDao is the data access object for the table im_security_ip_rules. +// You can define custom methods on it to extend its functionality as needed. +type imSecurityIpRulesDao struct { + *internal.ImSecurityIpRulesDao +} + +var ( + // ImSecurityIpRules is a globally accessible object for table im_security_ip_rules operations. + ImSecurityIpRules = imSecurityIpRulesDao{internal.NewImSecurityIpRulesDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/im_security_policy.go b/server/internal/dao/im_security_policy.go new file mode 100644 index 0000000..3fd9249 --- /dev/null +++ b/server/internal/dao/im_security_policy.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imSecurityPolicyDao is the data access object for the table im_security_policy. +// You can define custom methods on it to extend its functionality as needed. +type imSecurityPolicyDao struct { + *internal.ImSecurityPolicyDao +} + +var ( + // ImSecurityPolicy is a globally accessible object for table im_security_policy operations. + ImSecurityPolicy = imSecurityPolicyDao{internal.NewImSecurityPolicyDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/im_sensitive_word_groups.go b/server/internal/dao/im_sensitive_word_groups.go new file mode 100644 index 0000000..bd5d607 --- /dev/null +++ b/server/internal/dao/im_sensitive_word_groups.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imSensitiveWordGroupsDao is the data access object for the table im_sensitive_word_groups. +// You can define custom methods on it to extend its functionality as needed. +type imSensitiveWordGroupsDao struct { + *internal.ImSensitiveWordGroupsDao +} + +var ( + // ImSensitiveWordGroups is a globally accessible object for table im_sensitive_word_groups operations. + ImSensitiveWordGroups = imSensitiveWordGroupsDao{internal.NewImSensitiveWordGroupsDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/im_sensitive_words.go b/server/internal/dao/im_sensitive_words.go new file mode 100644 index 0000000..be86128 --- /dev/null +++ b/server/internal/dao/im_sensitive_words.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imSensitiveWordsDao is the data access object for the table im_sensitive_words. +// You can define custom methods on it to extend its functionality as needed. +type imSensitiveWordsDao struct { + *internal.ImSensitiveWordsDao +} + +var ( + // ImSensitiveWords is a globally accessible object for table im_sensitive_words operations. + ImSensitiveWords = imSensitiveWordsDao{internal.NewImSensitiveWordsDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/im_user_policy_group_members.go b/server/internal/dao/im_user_policy_group_members.go new file mode 100644 index 0000000..6dff14d --- /dev/null +++ b/server/internal/dao/im_user_policy_group_members.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imUserPolicyGroupMembersDao is the data access object for the table im_user_policy_group_members. +// You can define custom methods on it to extend its functionality as needed. +type imUserPolicyGroupMembersDao struct { + *internal.ImUserPolicyGroupMembersDao +} + +var ( + // ImUserPolicyGroupMembers is a globally accessible object for table im_user_policy_group_members operations. + ImUserPolicyGroupMembers = imUserPolicyGroupMembersDao{internal.NewImUserPolicyGroupMembersDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/im_user_policy_groups.go b/server/internal/dao/im_user_policy_groups.go new file mode 100644 index 0000000..0fffda4 --- /dev/null +++ b/server/internal/dao/im_user_policy_groups.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// imUserPolicyGroupsDao is the data access object for the table im_user_policy_groups. +// You can define custom methods on it to extend its functionality as needed. +type imUserPolicyGroupsDao struct { + *internal.ImUserPolicyGroupsDao +} + +var ( + // ImUserPolicyGroups is a globally accessible object for table im_user_policy_groups operations. + ImUserPolicyGroups = imUserPolicyGroupsDao{internal.NewImUserPolicyGroupsDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/internal/im_message_curfew_rules.go b/server/internal/dao/internal/im_message_curfew_rules.go new file mode 100644 index 0000000..c796a5b --- /dev/null +++ b/server/internal/dao/internal/im_message_curfew_rules.go @@ -0,0 +1,123 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImMessageCurfewRulesDao is the data access object for the table im_message_curfew_rules. +type ImMessageCurfewRulesDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImMessageCurfewRulesColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImMessageCurfewRulesColumns defines and stores column names for the table im_message_curfew_rules. +type ImMessageCurfewRulesColumns struct { + Id string // + TenantId string // + PolicyId string // + RuleCode string // + RuleName string // + DateType string // daily, weekday, weekend, custom + CustomDates string // YYYY-MM-DD values for custom date type + Weekdays string // ISO weekdays 1 through 7 + Timezone string // + StartTime string // + EndTime string // Less than start_time means crossing midnight + Scope string // private_message or group_message + WhitelistUserIds string // + WhitelistGroupIds string // + Message string // + Status string // 1 enabled, 2 disabled + Sort string // + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // + DeletedAt string // + DeletedBy string // +} + +// imMessageCurfewRulesColumns holds the columns for the table im_message_curfew_rules. +var imMessageCurfewRulesColumns = ImMessageCurfewRulesColumns{ + Id: "id", + TenantId: "tenant_id", + PolicyId: "policy_id", + RuleCode: "rule_code", + RuleName: "rule_name", + DateType: "date_type", + CustomDates: "custom_dates", + Weekdays: "weekdays", + Timezone: "timezone", + StartTime: "start_time", + EndTime: "end_time", + Scope: "scope", + WhitelistUserIds: "whitelist_user_ids", + WhitelistGroupIds: "whitelist_group_ids", + Message: "message", + Status: "status", + Sort: "sort", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedAt: "deleted_at", + DeletedBy: "deleted_by", +} + +// NewImMessageCurfewRulesDao creates and returns a new DAO object for table data access. +func NewImMessageCurfewRulesDao(handlers ...gdb.ModelHandler) *ImMessageCurfewRulesDao { + return &ImMessageCurfewRulesDao{ + group: "default", + table: "im_message_curfew_rules", + columns: imMessageCurfewRulesColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImMessageCurfewRulesDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImMessageCurfewRulesDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImMessageCurfewRulesDao) Columns() ImMessageCurfewRulesColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImMessageCurfewRulesDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImMessageCurfewRulesDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImMessageCurfewRulesDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/im_security_ip_rules.go b/server/internal/dao/internal/im_security_ip_rules.go new file mode 100644 index 0000000..5e67561 --- /dev/null +++ b/server/internal/dao/internal/im_security_ip_rules.go @@ -0,0 +1,109 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImSecurityIpRulesDao is the data access object for the table im_security_ip_rules. +type ImSecurityIpRulesDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImSecurityIpRulesColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImSecurityIpRulesColumns defines and stores column names for the table im_security_ip_rules. +type ImSecurityIpRulesColumns struct { + Id string // + TenantId string // + RuleType string // whitelist or blacklist + IpCidr string // IPv4, IPv6 or CIDR + Action string // allow, challenge, block, audit_only + SceneJson string // login, register, message, upload and other scenes + RiskLevel string // + Status string // 1 enabled, 2 disabled + ExpiresAt string // + Remark string // + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // + DeletedAt string // + DeletedBy string // +} + +// imSecurityIpRulesColumns holds the columns for the table im_security_ip_rules. +var imSecurityIpRulesColumns = ImSecurityIpRulesColumns{ + Id: "id", + TenantId: "tenant_id", + RuleType: "rule_type", + IpCidr: "ip_cidr", + Action: "action", + SceneJson: "scene_json", + RiskLevel: "risk_level", + Status: "status", + ExpiresAt: "expires_at", + Remark: "remark", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedAt: "deleted_at", + DeletedBy: "deleted_by", +} + +// NewImSecurityIpRulesDao creates and returns a new DAO object for table data access. +func NewImSecurityIpRulesDao(handlers ...gdb.ModelHandler) *ImSecurityIpRulesDao { + return &ImSecurityIpRulesDao{ + group: "default", + table: "im_security_ip_rules", + columns: imSecurityIpRulesColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImSecurityIpRulesDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImSecurityIpRulesDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImSecurityIpRulesDao) Columns() ImSecurityIpRulesColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImSecurityIpRulesDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImSecurityIpRulesDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImSecurityIpRulesDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/im_security_policy.go b/server/internal/dao/internal/im_security_policy.go new file mode 100644 index 0000000..12b402a --- /dev/null +++ b/server/internal/dao/internal/im_security_policy.go @@ -0,0 +1,123 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImSecurityPolicyDao is the data access object for the table im_security_policy. +type ImSecurityPolicyDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImSecurityPolicyColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImSecurityPolicyColumns defines and stores column names for the table im_security_policy. +type ImSecurityPolicyColumns struct { + Id string // + TenantId string // 0 means global fallback policy + PolicyVersion string // + PolicyName string // + PublishStatus string // + PasswordPolicy string // + LoginRateLimit string // + SensitiveWordPolicy string // + FileReviewPolicy string // + RealnamePolicy string // + RiskChallengePolicy string // + GlobalFriendEnabled string // Backend master switch for every add-friend entry + FriendDisabledPolicy string // Profile masking, QR and group-member restrictions + MessageCurfewEnabled string // Backend master switch for message curfew + Remark string // + PublishedAt string // + PublishedBy string // + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // + DeletedAt string // + DeletedBy string // +} + +// imSecurityPolicyColumns holds the columns for the table im_security_policy. +var imSecurityPolicyColumns = ImSecurityPolicyColumns{ + Id: "id", + TenantId: "tenant_id", + PolicyVersion: "policy_version", + PolicyName: "policy_name", + PublishStatus: "publish_status", + PasswordPolicy: "password_policy", + LoginRateLimit: "login_rate_limit", + SensitiveWordPolicy: "sensitive_word_policy", + FileReviewPolicy: "file_review_policy", + RealnamePolicy: "realname_policy", + RiskChallengePolicy: "risk_challenge_policy", + GlobalFriendEnabled: "global_friend_enabled", + FriendDisabledPolicy: "friend_disabled_policy", + MessageCurfewEnabled: "message_curfew_enabled", + Remark: "remark", + PublishedAt: "published_at", + PublishedBy: "published_by", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedAt: "deleted_at", + DeletedBy: "deleted_by", +} + +// NewImSecurityPolicyDao creates and returns a new DAO object for table data access. +func NewImSecurityPolicyDao(handlers ...gdb.ModelHandler) *ImSecurityPolicyDao { + return &ImSecurityPolicyDao{ + group: "default", + table: "im_security_policy", + columns: imSecurityPolicyColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImSecurityPolicyDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImSecurityPolicyDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImSecurityPolicyDao) Columns() ImSecurityPolicyColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImSecurityPolicyDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImSecurityPolicyDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImSecurityPolicyDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/im_sensitive_word_groups.go b/server/internal/dao/internal/im_sensitive_word_groups.go new file mode 100644 index 0000000..c130c56 --- /dev/null +++ b/server/internal/dao/internal/im_sensitive_word_groups.go @@ -0,0 +1,103 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImSensitiveWordGroupsDao is the data access object for the table im_sensitive_word_groups. +type ImSensitiveWordGroupsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImSensitiveWordGroupsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImSensitiveWordGroupsColumns defines and stores column names for the table im_sensitive_word_groups. +type ImSensitiveWordGroupsColumns struct { + Id string // + TenantId string // 0 means global dictionary + GroupCode string // + GroupName string // + Priority string // + Status string // 1 enabled, 2 disabled + Remark string // + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // + DeletedAt string // + DeletedBy string // +} + +// imSensitiveWordGroupsColumns holds the columns for the table im_sensitive_word_groups. +var imSensitiveWordGroupsColumns = ImSensitiveWordGroupsColumns{ + Id: "id", + TenantId: "tenant_id", + GroupCode: "group_code", + GroupName: "group_name", + Priority: "priority", + Status: "status", + Remark: "remark", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedAt: "deleted_at", + DeletedBy: "deleted_by", +} + +// NewImSensitiveWordGroupsDao creates and returns a new DAO object for table data access. +func NewImSensitiveWordGroupsDao(handlers ...gdb.ModelHandler) *ImSensitiveWordGroupsDao { + return &ImSensitiveWordGroupsDao{ + group: "default", + table: "im_sensitive_word_groups", + columns: imSensitiveWordGroupsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImSensitiveWordGroupsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImSensitiveWordGroupsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImSensitiveWordGroupsDao) Columns() ImSensitiveWordGroupsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImSensitiveWordGroupsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImSensitiveWordGroupsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImSensitiveWordGroupsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/im_sensitive_words.go b/server/internal/dao/internal/im_sensitive_words.go new file mode 100644 index 0000000..aa1b7ed --- /dev/null +++ b/server/internal/dao/internal/im_sensitive_words.go @@ -0,0 +1,115 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImSensitiveWordsDao is the data access object for the table im_sensitive_words. +type ImSensitiveWordsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImSensitiveWordsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImSensitiveWordsColumns defines and stores column names for the table im_sensitive_words. +type ImSensitiveWordsColumns struct { + Id string // + TenantId string // 0 means global dictionary + GroupId string // + Word string // + WordHash string // SHA-256 of normalized word + MatchType string // + SceneJson string // Enabled scenes + Action string // + Replacement string // + RiskLevel string // + Priority string // + Status string // 1 enabled, 2 disabled + Remark string // + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // + DeletedAt string // + DeletedBy string // +} + +// imSensitiveWordsColumns holds the columns for the table im_sensitive_words. +var imSensitiveWordsColumns = ImSensitiveWordsColumns{ + Id: "id", + TenantId: "tenant_id", + GroupId: "group_id", + Word: "word", + WordHash: "word_hash", + MatchType: "match_type", + SceneJson: "scene_json", + Action: "action", + Replacement: "replacement", + RiskLevel: "risk_level", + Priority: "priority", + Status: "status", + Remark: "remark", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedAt: "deleted_at", + DeletedBy: "deleted_by", +} + +// NewImSensitiveWordsDao creates and returns a new DAO object for table data access. +func NewImSensitiveWordsDao(handlers ...gdb.ModelHandler) *ImSensitiveWordsDao { + return &ImSensitiveWordsDao{ + group: "default", + table: "im_sensitive_words", + columns: imSensitiveWordsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImSensitiveWordsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImSensitiveWordsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImSensitiveWordsDao) Columns() ImSensitiveWordsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImSensitiveWordsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImSensitiveWordsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImSensitiveWordsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/im_tenant_config.go b/server/internal/dao/internal/im_tenant_config.go index 3e60943..16bc159 100644 --- a/server/internal/dao/internal/im_tenant_config.go +++ b/server/internal/dao/internal/im_tenant_config.go @@ -21,28 +21,36 @@ type ImTenantConfigDao struct { // ImTenantConfigColumns defines and stores column names for the table im_tenant_config. type ImTenantConfigColumns struct { - Id string // - TenantId string // - MaxUsers string // 0 means unlimited - MaxGroups string // 0 means unlimited - MaxGroupMembers string // 0 means unlimited - CreatedAt string // - CreatedBy string // - UpdatedAt string // - UpdatedBy string // + Id string // + TenantId string // + MaxUsers string // 0 means unlimited + MaxGroups string // 0 means unlimited + MaxGroupMembers string // 0 means unlimited + DefaultPolicyGroupId string // Default user policy group + SecurityPolicyVersion string // Published security policy version + GlobalFriendEnabled string // Published global friend switch + MessageCurfewEnabled string // Published message curfew switch + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // } // imTenantConfigColumns holds the columns for the table im_tenant_config. var imTenantConfigColumns = ImTenantConfigColumns{ - Id: "id", - TenantId: "tenant_id", - MaxUsers: "max_users", - MaxGroups: "max_groups", - MaxGroupMembers: "max_group_members", - CreatedAt: "created_at", - CreatedBy: "created_by", - UpdatedAt: "updated_at", - UpdatedBy: "updated_by", + Id: "id", + TenantId: "tenant_id", + MaxUsers: "max_users", + MaxGroups: "max_groups", + MaxGroupMembers: "max_group_members", + DefaultPolicyGroupId: "default_policy_group_id", + SecurityPolicyVersion: "security_policy_version", + GlobalFriendEnabled: "global_friend_enabled", + MessageCurfewEnabled: "message_curfew_enabled", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", } // NewImTenantConfigDao creates and returns a new DAO object for table data access. diff --git a/server/internal/dao/internal/im_user_policy_group_members.go b/server/internal/dao/internal/im_user_policy_group_members.go new file mode 100644 index 0000000..17b3ccd --- /dev/null +++ b/server/internal/dao/internal/im_user_policy_group_members.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImUserPolicyGroupMembersDao is the data access object for the table im_user_policy_group_members. +type ImUserPolicyGroupMembersDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImUserPolicyGroupMembersColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImUserPolicyGroupMembersColumns defines and stores column names for the table im_user_policy_group_members. +type ImUserPolicyGroupMembersColumns struct { + Id string // + TenantId string // + GroupId string // + UserId string // User ID in the tenant database + Status string // 1 enabled, 2 disabled + ValidFrom string // + ValidUntil string // + Remark string // + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // + DeletedAt string // + DeletedBy string // +} + +// imUserPolicyGroupMembersColumns holds the columns for the table im_user_policy_group_members. +var imUserPolicyGroupMembersColumns = ImUserPolicyGroupMembersColumns{ + Id: "id", + TenantId: "tenant_id", + GroupId: "group_id", + UserId: "user_id", + Status: "status", + ValidFrom: "valid_from", + ValidUntil: "valid_until", + Remark: "remark", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedAt: "deleted_at", + DeletedBy: "deleted_by", +} + +// NewImUserPolicyGroupMembersDao creates and returns a new DAO object for table data access. +func NewImUserPolicyGroupMembersDao(handlers ...gdb.ModelHandler) *ImUserPolicyGroupMembersDao { + return &ImUserPolicyGroupMembersDao{ + group: "default", + table: "im_user_policy_group_members", + columns: imUserPolicyGroupMembersColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImUserPolicyGroupMembersDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImUserPolicyGroupMembersDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImUserPolicyGroupMembersDao) Columns() ImUserPolicyGroupMembersColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImUserPolicyGroupMembersDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImUserPolicyGroupMembersDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImUserPolicyGroupMembersDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/im_user_policy_groups.go b/server/internal/dao/internal/im_user_policy_groups.go new file mode 100644 index 0000000..8cbbe72 --- /dev/null +++ b/server/internal/dao/internal/im_user_policy_groups.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ImUserPolicyGroupsDao is the data access object for the table im_user_policy_groups. +type ImUserPolicyGroupsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns ImUserPolicyGroupsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// ImUserPolicyGroupsColumns defines and stores column names for the table im_user_policy_groups. +type ImUserPolicyGroupsColumns struct { + Id string // + TenantId string // 0 means global default + GroupCode string // Stable policy group code + GroupName string // Policy group name + Priority string // Higher value has higher priority + Status string // 1 enabled, 2 disabled + PolicyJson string // Login, friend, message, group and file policies + IpWhitelist string // IPv4/IPv6 or CIDR list + IpBlacklist string // IPv4/IPv6 or CIDR list + Version string // Optimistic-lock/config version + Remark string // + CreatedAt string // + CreatedBy string // + UpdatedAt string // + UpdatedBy string // + DeletedAt string // + DeletedBy string // +} + +// imUserPolicyGroupsColumns holds the columns for the table im_user_policy_groups. +var imUserPolicyGroupsColumns = ImUserPolicyGroupsColumns{ + Id: "id", + TenantId: "tenant_id", + GroupCode: "group_code", + GroupName: "group_name", + Priority: "priority", + Status: "status", + PolicyJson: "policy_json", + IpWhitelist: "ip_whitelist", + IpBlacklist: "ip_blacklist", + Version: "version", + Remark: "remark", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedAt: "deleted_at", + DeletedBy: "deleted_by", +} + +// NewImUserPolicyGroupsDao creates and returns a new DAO object for table data access. +func NewImUserPolicyGroupsDao(handlers ...gdb.ModelHandler) *ImUserPolicyGroupsDao { + return &ImUserPolicyGroupsDao{ + group: "default", + table: "im_user_policy_groups", + columns: imUserPolicyGroupsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *ImUserPolicyGroupsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *ImUserPolicyGroupsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *ImUserPolicyGroupsDao) Columns() ImUserPolicyGroupsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *ImUserPolicyGroupsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *ImUserPolicyGroupsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *ImUserPolicyGroupsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/library/casbin/casbin_test.go b/server/internal/library/casbin/casbin_test.go index 6dafe7a..ef05df9 100644 --- a/server/internal/library/casbin/casbin_test.go +++ b/server/internal/library/casbin/casbin_test.go @@ -1,67 +1,66 @@ -// Package casbin -// @Link https://github.com/bufanyun/hotgo -// @Copyright Copyright (c) 2023 HotGo CLI -// @Author Ms <133814250@qq.com> -// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE package casbin import ( - "context" "net/http" "testing" + + casbinv2 "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" ) -// TestNew description -func TestNew(t *testing.T) { +const testModel = ` +[request_definition] +r = sub, obj, act - InitEnforcer(context.TODO()) +[policy_definition] +p = sub, obj, act - user := "admin" - path := "/" - method := http.MethodGet - t.Logf("\nuser:%v\npath:%v\nmethod:%v", user, path, method) +[role_definition] +g = _, _ - ok, err := Enforcer.DeletePermissionsForUser(user) +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) +` + +func TestEnforcerPermissions(t *testing.T) { + m, err := model.NewModelFromString(testModel) if err != nil { - t.Error(err) + t.Fatalf("创建Casbin测试模型失败: %v", err) } - t.Logf("delete user premission:%v", ok) - CheckPremission(t, user, path, method) - AddPremission(t, user, "*", ActionAll) - CheckPremission(t, user, path, method) - - user1 := "user1" - path1 := "/api/v1/*" - checkPathTrue := "/api/v1/user/list" - checkPathFalse := "/api/v2/user/list" - AddPremission(t, user1, path1, ActionAll) - CheckPremission(t, user1, checkPathTrue, ActionPost) - CheckPremission(t, user1, checkPathFalse, http.MethodGet) - CheckPremission(t, user1, checkPathTrue, http.MethodGet) - CheckPremission(t, user1, "/api/v1/user/list2", http.MethodGet) - - ok, err = Enforcer.DeletePermissionsForUser(user1) + enforcer, err := casbinv2.NewEnforcer(m) if err != nil { - t.Error(err) + t.Fatalf("创建Casbin测试实例失败: %v", err) } - t.Logf("delete user premission:%v", ok) - CheckPremission(t, user1, "/api/v1/user1/list", http.MethodGet) + + assertPermission(t, enforcer, "admin", "/", http.MethodGet, false) + if ok, addErr := enforcer.AddPolicy("admin", "*", ActionAll); addErr != nil || !ok { + t.Fatalf("添加管理员策略失败: ok=%v err=%v", ok, addErr) + } + assertPermission(t, enforcer, "admin", "/", http.MethodGet, true) + + if ok, addErr := enforcer.AddPolicy("user1", "/api/v1/*", ActionAll); addErr != nil || !ok { + t.Fatalf("添加用户策略失败: ok=%v err=%v", ok, addErr) + } + assertPermission(t, enforcer, "user1", "/api/v1/user/list", http.MethodPost, true) + assertPermission(t, enforcer, "user1", "/api/v1/user/list", http.MethodGet, true) + assertPermission(t, enforcer, "user1", "/api/v2/user/list", http.MethodGet, false) + + if ok, deleteErr := enforcer.DeletePermissionsForUser("user1"); deleteErr != nil || !ok { + t.Fatalf("删除用户策略失败: ok=%v err=%v", ok, deleteErr) + } + assertPermission(t, enforcer, "user1", "/api/v1/user/list", http.MethodGet, false) } -// CheckPremission description -func CheckPremission(t *testing.T, user string, path string, method string) { - ok, err := Enforcer.Enforce(user, path, method) +func assertPermission(t *testing.T, enforcer *casbinv2.Enforcer, user, path, method string, expected bool) { + t.Helper() + actual, err := enforcer.Enforce(user, path, method) if err != nil { - t.Error(err) + t.Fatalf("检查权限失败: %v", err) } - t.Logf("check \tuser[%s] \tpremission[%s] \tpath[%s] \tallow[%v]", user, method, path, ok) -} - -// Add description -func AddPremission(t *testing.T, user string, path string, method string) { - ok, err := Enforcer.AddPolicy(user, path, method) - if err != nil { - t.Error(err) + if actual != expected { + t.Fatalf("权限结果不符 user=%s path=%s method=%s expected=%v actual=%v", user, path, method, expected, actual) } - t.Logf("add \tuser[%s] \tpremission[%s] \tpath[%s] \tresult[%v]", user, method, path, ok) } diff --git a/server/internal/library/network/tcp/client.go b/server/internal/library/network/tcp/client.go index ad0a1f9..b3f6d4d 100644 --- a/server/internal/library/network/tcp/client.go +++ b/server/internal/library/network/tcp/client.go @@ -281,6 +281,8 @@ func (client *Client) handleRoutineTask(ctx context.Context, task func()) { // Conn 获取当前连接 func (client *Client) Conn() *Conn { + client.mutex.Lock() + defer client.mutex.Unlock() return client.conn } diff --git a/server/internal/library/network/tcp/rpc.go b/server/internal/library/network/tcp/rpc.go index 2ebd5bf..5c1e3c8 100644 --- a/server/internal/library/network/tcp/rpc.go +++ b/server/internal/library/network/tcp/rpc.go @@ -43,7 +43,6 @@ func (r *RPC) Request(ctx context.Context, msgId string, send func()) (res inter defer func() { isClose.Set(true) - close(resCh) r.popCallback(msgId) }() @@ -57,8 +56,13 @@ func (r *RPC) Request(ctx context.Context, msgId string, send func()) (res inter r.task(ctx, send) + timer := time.NewTimer(time.Second * RPCTimeout) + defer timer.Stop() select { - case <-time.After(time.Second * RPCTimeout): + case <-ctx.Done(): + err = gerror.Wrap(ctx.Err(), "RPC request canceled") + return + case <-timer.C: err = gerror.New("RPC response timeout") return case got := <-resCh: diff --git a/server/internal/library/network/tcp/tcp_example_test.go b/server/internal/library/network/tcp/tcp_example_test.go index b23ef20..8d5e0ce 100644 --- a/server/internal/library/network/tcp/tcp_example_test.go +++ b/server/internal/library/network/tcp/tcp_example_test.go @@ -1,21 +1,14 @@ -// Package tcp_test -// @Link https://github.com/bufanyun/hotgo -// @Copyright Copyright (c) 2023 HotGo CLI -// @Author Ms <133814250@qq.com> -// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE package tcp_test import ( "context" "fmt" - "github.com/gogf/gf/v2/os/gctx" - "github.com/gogf/gf/v2/test/gtest" - "hotgo/internal/library/network/tcp" + "net" "testing" "time" -) -var T *testing.T // 声明一个全局的 *testing.T 变量 + "hotgo/internal/library/network/tcp" +) type TestMsgReq struct { Name string `json:"name"` @@ -33,101 +26,95 @@ type TestRPCMsgRes struct { tcp.ServerRes } -func onTestMsg(ctx context.Context, req *TestMsgReq) { - fmt.Printf("服务器收到消息 ==> onTestMsg:%+v\n", req) - conn := tcp.ConnFromCtx(ctx) - gtest.C(T, func(t *gtest.T) { - t.AssertNE(conn, nil) - }) - - res := new(TestMsgRes) - res.Message = fmt.Sprintf("你的名字:%v", req.Name) - conn.Send(ctx, res) -} - -func onResponseTestMsg(ctx context.Context, req *TestMsgRes) { - fmt.Printf("客户端收到响应消息 ==> TestMsgRes:%+v\n", req) - err := req.GetError() - gtest.C(T, func(t *gtest.T) { - t.AssertNil(err) - }) -} - -func onTestRPCMsg(ctx context.Context, req *TestRPCMsgReq) (res *TestRPCMsgRes, err error) { - fmt.Printf("服务器收到消息 ==> onTestRPCMsg:%+v\n", req) - res = new(TestRPCMsgRes) - res.Message = fmt.Sprintf("你的名字:%v", req.Name) - return -} - -func startTCPServer() { - serv := tcp.NewServer(&tcp.ServerConfig{ - Name: "hotgo", - Addr: ":8002", - }) - - // 注册路由 - serv.RegisterRouter( - onTestMsg, - ) - - // 注册RPC路由 - serv.RegisterRPCRouter( - onTestRPCMsg, - ) - - // 服务监听 - err := serv.Listen() - gtest.C(T, func(t *gtest.T) { - t.AssertNil(err) - }) -} - -// 一个基本的消息收发 func TestSendMsg(t *testing.T) { - T = t - go startTCPServer() - - ctx := gctx.New() - client := tcp.NewClient(&tcp.ClientConfig{ - Addr: "127.0.0.1:8002", + address := freeTCPAddress(t) + messageResponse := make(chan string, 1) + server := tcp.NewServer(&tcp.ServerConfig{Name: "hotgo-test", Addr: address}) + server.RegisterRouter(func(ctx context.Context, req *TestMsgReq) { + res := &TestMsgRes{} + res.SetMessage(fmt.Sprintf("你的名字:%s", req.Name)) + if err := tcp.ConnFromCtx(ctx).Send(ctx, res); err != nil { + messageResponse <- "send-error:" + err.Error() + } }) - - // 注册路由 - client.RegisterRouter( - onResponseTestMsg, - ) - + server.RegisterRPCRouter(func(ctx context.Context, req *TestRPCMsgReq) (*TestRPCMsgRes, error) { + res := &TestRPCMsgRes{} + res.SetMessage(fmt.Sprintf("你的名字:%s", req.Name)) + return res, nil + }) + serverErrors := make(chan error, 1) go func() { - err := client.Start() - gtest.C(T, func(t *gtest.T) { - t.AssertNil(err) - }) + serverErrors <- server.Listen() }() + defer server.Close() - // 确保服务都启动完成 - time.Sleep(time.Second * 1) - - // 拿到客户端的连接 - conn := client.Conn() - gtest.C(T, func(t *gtest.T) { - t.AssertNE(conn, nil) + client := tcp.NewClient(&tcp.ClientConfig{ + Addr: address, + Timeout: time.Second, + ConnectInterval: 10 * time.Millisecond, + MaxConnectCount: 100, }) - - // 向服务器发送tcp消息,不会阻塞程序执行 - err := conn.Send(ctx, &TestMsgReq{Name: "Tom"}) - gtest.C(T, func(t *gtest.T) { - t.AssertNil(err) + client.RegisterRouter(func(ctx context.Context, res *TestMsgRes) { + messageResponse <- res.Message }) + if err := client.Start(); err != nil { + t.Fatalf("启动TCP客户端失败: %v", err) + } + defer client.Stop() - // 向服务器发送rpc消息,会等待服务器响应结果,直到拿到结果或响应超时才会继续 - var res TestRPCMsgRes - if err = conn.RequestScan(ctx, &TestRPCMsgReq{Name: "Tony"}, &res); err != nil { - gtest.C(T, func(t *gtest.T) { - t.AssertNil(err) - }) + conn := waitForConnection(t, client, serverErrors) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := conn.Send(ctx, &TestMsgReq{Name: "Tom"}); err != nil { + t.Fatalf("发送TCP消息失败: %v", err) + } + select { + case message := <-messageResponse: + if message != "你的名字:Tom" { + t.Fatalf("TCP响应不符: %s", message) + } + case <-ctx.Done(): + t.Fatal("等待TCP响应超时") } - fmt.Printf("客户端收到RPC消息响应 ==> TestRPCMsgRes:%+v\n", res) - time.Sleep(time.Second * 1) + var rpcResponse TestRPCMsgRes + if err := conn.RequestScan(ctx, &TestRPCMsgReq{Name: "Tony"}, &rpcResponse); err != nil { + t.Fatalf("发送RPC消息失败: %v", err) + } + if rpcResponse.Message != "你的名字:Tony" { + t.Fatalf("RPC响应不符: %s", rpcResponse.Message) + } +} + +func freeTCPAddress(t *testing.T) string { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("申请测试端口失败: %v", err) + } + address := listener.Addr().String() + if err = listener.Close(); err != nil { + t.Fatalf("释放测试端口失败: %v", err) + } + return address +} + +func waitForConnection(t *testing.T, client *tcp.Client, serverErrors <-chan error) *tcp.Conn { + t.Helper() + deadline := time.NewTimer(2 * time.Second) + defer deadline.Stop() + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + for { + select { + case err := <-serverErrors: + t.Fatalf("TCP服务启动失败: %v", err) + case <-deadline.C: + t.Fatal("等待TCP连接超时") + case <-ticker.C: + if conn := client.Conn(); conn != nil { + return conn + } + } + } } diff --git a/server/internal/library/storager/upload_local.go b/server/internal/library/storager/upload_local.go index 3f7fa93..a5032d9 100644 --- a/server/internal/library/storager/upload_local.go +++ b/server/internal/library/storager/upload_local.go @@ -13,6 +13,7 @@ import ( "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" + "io" "os" "path/filepath" "sort" @@ -137,20 +138,53 @@ func (d *LocalDrive) UploadPart(ctx context.Context, in *UploadPartParams) (res // MergePartFile 合并分片文件 func MergePartFile(srcPath, dstPath string) (err error) { - dir, err := os.ReadDir(srcPath) + entries, err := os.ReadDir(srcPath) if err != nil { return err } - sort.Slice(dir, func(i, j int) bool { - fiIndex, _ := strconv.Atoi(dir[i].Name()) - fjIndex, _ := strconv.Atoi(dir[j].Name()) - return fiIndex < fjIndex + type filePart struct { + index int + path string + } + parts := make([]filePart, 0, len(entries)) + for _, entry := range entries { + if entry.IsDir() { + continue + } + index, parseErr := strconv.Atoi(entry.Name()) + if parseErr != nil || index <= 0 { + return gerror.Newf("无效的分片文件名:%s", entry.Name()) + } + parts = append(parts, filePart{index: index, path: filepath.Join(srcPath, entry.Name())}) + } + if len(parts) == 0 { + return gerror.New("没有可合并的分片文件") + } + sort.Slice(parts, func(i, j int) bool { + return parts[i].index < parts[j].index }) - for _, file := range dir { - filePath := filepath.Join(srcPath, file.Name()) - if err = gfile.PutBytesAppend(dstPath, gfile.GetBytes(filePath)); err != nil { - return err + if err = os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { + return gerror.Wrap(err, "创建目标目录失败") + } + dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + return gerror.Wrap(err, "创建合并文件失败") + } + defer dstFile.Close() + + for _, part := range parts { + srcFile, openErr := os.Open(part.path) + if openErr != nil { + return gerror.Wrapf(openErr, "打开分片%d失败", part.index) + } + _, copyErr := io.Copy(dstFile, srcFile) + closeErr := srcFile.Close() + if copyErr != nil { + return gerror.Wrapf(copyErr, "合并分片%d失败", part.index) + } + if closeErr != nil { + return gerror.Wrapf(closeErr, "关闭分片%d失败", part.index) } } - return + return dstFile.Sync() } diff --git a/server/internal/library/storager/upload_local_test.go b/server/internal/library/storager/upload_local_test.go index 8dea7bf..1891b37 100644 --- a/server/internal/library/storager/upload_local_test.go +++ b/server/internal/library/storager/upload_local_test.go @@ -1,61 +1,52 @@ package storager import ( - "crypto/md5" - "errors" - "fmt" - "io" + "bytes" "os" + "path/filepath" "testing" ) func TestMergePartFile(t *testing.T) { - srcFile, _ := os.Open("1.zip") - defer srcFile.Close() - fileInfo, err := srcFile.Stat() - if err != nil { - t.Error(err) + tempDir := t.TempDir() + partDir := filepath.Join(tempDir, "parts") + if err := os.MkdirAll(partDir, 0o755); err != nil { + t.Fatal(err) } - hash := md5.New() - _, err = io.Copy(hash, srcFile) - sum := hash.Sum(nil) - fileHash := fmt.Sprintf("%x", sum) - var singleSize = int64(1024 * 1024 * 100) - chunks := int(fileInfo.Size() / singleSize) - if fileInfo.Size()%singleSize != 0 { - chunks += 1 + parts := map[string][]byte{ + "1": []byte("first-"), + "2": []byte("second-"), + "10": []byte("tenth"), } - srcFile.Seek(0, io.SeekStart) - for j := 0; j < chunks; j++ { - partSize := singleSize - if j == chunks-1 { - partSize = fileInfo.Size() - int64(j)*singleSize + for name, content := range parts { + if err := os.WriteFile(filepath.Join(partDir, name), content, 0o644); err != nil { + t.Fatal(err) } - partData := make([]byte, partSize) - _, err = io.ReadFull(srcFile, partData) - pf, _ := os.Create(fmt.Sprintf("tmp/%d", j+1)) - _, err = pf.Write(partData) - if err != nil { - t.Error(err) - return - } - pf.Close() } - err = MergePartFile("tmp/", "2.zip") + destination := filepath.Join(tempDir, "merged.bin") + if err := os.WriteFile(destination, []byte("stale-data"), 0o644); err != nil { + t.Fatal(err) + } + if err := MergePartFile(partDir, destination); err != nil { + t.Fatalf("合并分片失败: %v", err) + } + + actual, err := os.ReadFile(destination) if err != nil { - t.Error(err) - return + t.Fatal(err) } + expected := []byte("first-second-tenth") + if !bytes.Equal(actual, expected) { + t.Fatalf("合并内容不符 expected=%q actual=%q", expected, actual) + } +} - f2, _ := os.Open("2.zip") - hash2 := md5.New() - _, err = io.Copy(hash2, f2) - sum2 := hash.Sum(nil) - fileHash2 := fmt.Sprintf("%x", sum2) - if fileHash != fileHash2 { - t.Error(errors.New("hash mismatch")) +func TestMergePartFileRejectsEmptyDirectory(t *testing.T) { + partDir := t.TempDir() + err := MergePartFile(partDir, filepath.Join(t.TempDir(), "merged.bin")) + if err == nil { + t.Fatal("空分片目录应返回错误") } - } diff --git a/server/internal/logic/sys/im_admin_query_client.go b/server/internal/logic/sys/im_admin_query_client.go index efeeb8f..3a460d2 100644 --- a/server/internal/logic/sys/im_admin_query_client.go +++ b/server/internal/logic/sys/im_admin_query_client.go @@ -150,7 +150,7 @@ func normalizeImAdminValue(target string, value any) any { parts = append(parts, text) } return strings.Join(parts, ", ") - case "profileJson", "trustedJson", "accessJson": + case "profileJson", "trustedJson", "accessJson", "head", "content", "metadata", "atUsers": return normalizeImAdminJSONString(value) default: return value diff --git a/server/internal/logic/sys/im_dashboard.go b/server/internal/logic/sys/im_dashboard.go new file mode 100644 index 0000000..b9b4e9b --- /dev/null +++ b/server/internal/logic/sys/im_dashboard.go @@ -0,0 +1,38 @@ +package sys + +import ( + "context" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + "net/url" +) + +type sSysImDashboard struct{} + +func NewSysImDashboard() *sSysImDashboard { return &sSysImDashboard{} } + +func init() { service.RegisterSysImDashboard(NewSysImDashboard()) } + +func (s *sSysImDashboard) Overview(ctx context.Context, in *sysin.ImDashboardOverviewInp) (*sysin.ImDashboardOverviewModel, error) { + tenant, err := resolveImAdminTenant(ctx, in.TenantId) + if err != nil { + return nil, err + } + return imAdminQueryOne[sysin.ImDashboardOverviewModel](ctx, tenant, + "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/dashboard/overview", nil) +} + +func (s *sSysImDashboard) Trends(ctx context.Context, in *sysin.ImDashboardTrendsInp) ([]*sysin.ImDashboardTrendModel, error) { + if err := in.Filter(ctx); err != nil { + return nil, err + } + tenant, err := resolveImAdminTenant(ctx, in.TenantId) + if err != nil { + return nil, err + } + values := addPageQuery(url.Values{}, 1, in.Days) + values.Set("days", intToString(in.Days)) + list, _, err := imAdminQueryList[sysin.ImDashboardTrendModel](ctx, tenant, + "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/dashboard/trends", values, nil) + return list, err +} diff --git a/server/internal/logic/sys/im_login_audit.go b/server/internal/logic/sys/im_login_audit.go new file mode 100644 index 0000000..e82854a --- /dev/null +++ b/server/internal/logic/sys/im_login_audit.go @@ -0,0 +1,155 @@ +package sys + +import ( + "context" + "fmt" + "hotgo/internal/consts" + "hotgo/internal/library/contexts" + "hotgo/internal/library/imkafka" + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + "hotgo/utility/convert" + "hotgo/utility/excel" + "net/url" + "strconv" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gctx" +) + +type sSysImUserLoginHistory struct{} +type sSysImLoginFailureLog struct{} + +func NewSysImUserLoginHistory() *sSysImUserLoginHistory { return &sSysImUserLoginHistory{} } +func NewSysImLoginFailureLog() *sSysImLoginFailureLog { return &sSysImLoginFailureLog{} } + +func init() { + service.RegisterSysImUserLoginHistory(NewSysImUserLoginHistory()) + service.RegisterSysImLoginFailureLog(NewSysImLoginFailureLog()) +} + +func (s *sSysImUserLoginHistory) List(ctx context.Context, in *sysin.ImUserLoginHistoryListInp) ([]*sysin.ImUserLoginHistoryModel, int, error) { + if err := in.Filter(ctx); err != nil { + return nil, 0, err + } + tenant, err := resolveImAdminTenant(ctx, in.TenantId) + if err != nil { + return nil, 0, err + } + values := addPageQuery(url.Values{}, in.Page, in.PerPage) + setQuery(values, "user_id", in.UserId) + setQuery(values, "account", in.Account) + setQuery(values, "login_type", in.LoginType) + setQuery(values, "event_type", in.EventType) + setQuery(values, "platform", in.Platform) + setQuery(values, "ip", in.Ip) + addTimeRangeQuery(values, "login_at", in.LoginAt) + return imAdminQueryList[sysin.ImUserLoginHistoryModel](ctx, tenant, + "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/user-login-logs", values, nil) +} + +func (s *sSysImUserLoginHistory) Export(ctx context.Context, in *sysin.ImUserLoginHistoryListInp) error { + list, total, err := s.List(ctx, in) + if err != nil { + return err + } + exports := make([]sysin.ImLoginAuditExportModel, 0, len(list)) + for _, item := range list { + exports = append(exports, sysin.ImLoginAuditExportModel{Id: item.Id, UserId: item.UserId, AccountMask: item.AccountMask, EventType: item.EventType, Platform: item.Platform, Ip: item.Ip, Region: item.Region, RiskLevel: item.RiskLevel, OccurredAt: item.LoginAt}) + } + return exportLoginAudit(ctx, exports, total, in.Page, in.PerPage, "用户登录历史") +} + +func (s *sSysImLoginFailureLog) List(ctx context.Context, in *sysin.ImLoginFailureLogListInp) ([]*sysin.ImLoginFailureLogModel, int, error) { + if err := in.Filter(ctx); err != nil { + return nil, 0, err + } + tenant, err := resolveImAdminTenant(ctx, in.TenantId) + if err != nil { + return nil, 0, err + } + values := addPageQuery(url.Values{}, in.Page, in.PerPage) + setQuery(values, "user_id", in.UserId) + setQuery(values, "account", in.Account) + setQuery(values, "failure_type", in.FailureType) + setQuery(values, "platform", in.Platform) + setQuery(values, "ip", in.Ip) + addTimeRangeQuery(values, "attempt_at", in.AttemptAt) + return imAdminQueryList[sysin.ImLoginFailureLogModel](ctx, tenant, + "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/login-failure-logs", values, nil) +} + +func (s *sSysImLoginFailureLog) View(ctx context.Context, in *sysin.ImLoginFailureViewInp) (*sysin.ImLoginFailureLogModel, error) { + if err := in.Filter(ctx); err != nil { + return nil, err + } + tenant, err := resolveImAdminTenant(ctx, in.TenantId) + if err != nil { + return nil, err + } + return imAdminQueryOne[sysin.ImLoginFailureLogModel](ctx, tenant, + "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/login-failure-logs/"+strconv.FormatInt(in.Id, 10), nil) +} + +func (s *sSysImLoginFailureLog) Export(ctx context.Context, in *sysin.ImLoginFailureLogListInp) error { + list, total, err := s.List(ctx, in) + if err != nil { + return err + } + exports := make([]sysin.ImLoginAuditExportModel, 0, len(list)) + for _, item := range list { + exports = append(exports, sysin.ImLoginAuditExportModel{Id: item.Id, UserId: item.UserId, AccountMask: item.AccountMask, EventType: item.FailureType, Platform: item.Platform, Ip: item.Ip, Region: item.Region, RiskLevel: item.RiskLevel, Reason: item.Reason, OccurredAt: item.AttemptAt}) + } + return exportLoginAudit(ctx, exports, total, in.Page, in.PerPage, "登录失败日志") +} + +func exportLoginAudit(ctx context.Context, exports []sysin.ImLoginAuditExportModel, total int, page int, perPage int, label string) error { + tags, err := convert.GetEntityDescTags(sysin.ImLoginAuditExportModel{}) + if err != nil { + return err + } + fileName := "导出" + label + "-" + gctx.CtxId(ctx) + sheetName := fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", total, form.CalPageCount(total, perPage), page, len(exports)) + return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName) +} + +func (s *sSysImLoginFailureLog) AddBlacklist(ctx context.Context, in *sysin.ImLoginFailureAddBlacklistInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + remark := in.Remark + if remark == "" { + remark = "由登录失败日志加入" + } + return service.SysImSecurityPolicy().IPRuleEdit(ctx, &sysin.ImSecurityIPRuleEditInp{ + TenantId: in.TenantId, RuleType: "blacklist", IpCidr: in.Ip, Action: "deny", + SceneJson: gjson.New([]string{"login"}), RiskLevel: "high", Status: consts.StatusEnabled, Remark: remark, + }) +} + +func (s *sSysImLoginFailureLog) BanUser(ctx context.Context, in *sysin.ImLoginFailureBanUserInp) (*sysin.ImUserCommandModel, error) { + if err := in.Filter(ctx); err != nil { + return nil, err + } + tenant, err := resolveImAdminTenant(ctx, in.TenantId) + if err != nil { + return nil, err + } + user := contexts.GetUser(ctx) + actorName := "" + if user != nil { + actorName = user.RealName + } + command, err := imkafka.PublishAdminCommand(ctx, imkafka.AdminCommandInput{ + TenantID: tenant.Id, TenantCode: tenant.Code, EventType: consts.ImAdminCommandUserBan, + BizType: "user", BizID: strconv.FormatInt(in.UserId, 10), + Actor: &imkafka.Actor{Type: "admin", ID: strconv.FormatInt(contexts.GetUserId(ctx), 10), DisplayName: actorName}, + Payload: map[string]any{"user_id": strconv.FormatInt(in.UserId, 10), "target_user_id": strconv.FormatInt(in.UserId, 10), "reason": in.Reason}, + CreatedBy: contexts.GetUserId(ctx), + }) + if err != nil { + return nil, err + } + return &sysin.ImUserCommandModel{CommandID: command.CommandID, EventID: command.EventID, Status: command.Status, TenantID: command.TenantID, TenantCode: command.TenantCode, EventType: command.EventType, IdempotencyKey: command.IdempotencyKey}, nil +} diff --git a/server/internal/logic/sys/im_message_admin.go b/server/internal/logic/sys/im_message_admin.go new file mode 100644 index 0000000..34f6c9c --- /dev/null +++ b/server/internal/logic/sys/im_message_admin.go @@ -0,0 +1,173 @@ +package sys + +import ( + "context" + "fmt" + "hotgo/internal/consts" + "hotgo/internal/library/contexts" + "hotgo/internal/library/imkafka" + "hotgo/internal/model/input/form" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + "hotgo/utility/convert" + "hotgo/utility/excel" + "net/url" + "strconv" + + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/util/gconv" +) + +type sSysImPrivateMessage struct{} +type sSysImGroupMessage struct{} + +func NewSysImPrivateMessage() *sSysImPrivateMessage { return &sSysImPrivateMessage{} } +func NewSysImGroupMessage() *sSysImGroupMessage { return &sSysImGroupMessage{} } + +func init() { + service.RegisterSysImPrivateMessage(NewSysImPrivateMessage()) + service.RegisterSysImGroupMessage(NewSysImGroupMessage()) +} + +func (s *sSysImPrivateMessage) List(ctx context.Context, in *sysin.ImPrivateMessageListInp) ([]*sysin.ImMessageListModel, int, error) { + if err := in.Filter(ctx); err != nil { + return nil, 0, err + } + values := addPageQuery(url.Values{}, in.Page, in.PerPage) + setQuery(values, "topic", in.Topic) + setQuery(values, "from_user_id", in.FromUserId) + setQuery(values, "to_user_id", in.ToUserId) + setQuery(values, "content_type", in.ContentType) + setQuery(values, "status", in.Status) + setQuery(values, "keyword", in.Keyword) + addTimeRangeQuery(values, "created_at", in.CreatedAt) + return queryImMessages(ctx, in.TenantId, "private-messages", values) +} + +func (s *sSysImGroupMessage) List(ctx context.Context, in *sysin.ImGroupMessageListInp) ([]*sysin.ImMessageListModel, int, error) { + if err := in.Filter(ctx); err != nil { + return nil, 0, err + } + values := addPageQuery(url.Values{}, in.Page, in.PerPage) + setQuery(values, "topic", in.Topic) + setQuery(values, "sender_user_id", in.SenderUserId) + setQuery(values, "content_type", in.ContentType) + setQuery(values, "status", in.Status) + setQuery(values, "keyword", in.Keyword) + addTimeRangeQuery(values, "created_at", in.CreatedAt) + return queryImMessages(ctx, in.TenantId, "group-messages", values) +} + +func queryImMessages(ctx context.Context, tenantId int64, resource string, values url.Values) ([]*sysin.ImMessageListModel, int, error) { + tenant, err := resolveImAdminTenant(ctx, tenantId) + if err != nil { + return nil, 0, err + } + return imAdminQueryList[sysin.ImMessageListModel](ctx, tenant, + "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/"+resource, values, + map[string]string{"sender_user_id": "fromUserId", "sent_at": "createdAt", "message_type": "contentType", "content_summary": "contentExcerpt"}) +} + +func (s *sSysImPrivateMessage) View(ctx context.Context, in *sysin.ImMessageViewInp) (*sysin.ImMessageViewModel, error) { + return viewImMessage(ctx, in) +} + +func (s *sSysImGroupMessage) View(ctx context.Context, in *sysin.ImMessageViewInp) (*sysin.ImMessageViewModel, error) { + return viewImMessage(ctx, in) +} + +func viewImMessage(ctx context.Context, in *sysin.ImMessageViewInp) (*sysin.ImMessageViewModel, error) { + if err := in.Filter(ctx); err != nil { + return nil, err + } + tenant, err := resolveImAdminTenant(ctx, in.TenantId) + if err != nil { + return nil, err + } + path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/messages/" + url.PathEscape(in.Topic) + "/" + strconv.FormatInt(in.SeqId, 10) + return imAdminQueryOne[sysin.ImMessageViewModel](ctx, tenant, path, + map[string]string{"sender_user_id": "fromUserId", "sent_at": "createdAt", "message_type": "contentType"}) +} + +func (s *sSysImPrivateMessage) Hide(ctx context.Context, in *sysin.ImMessageActionInp) (*sysin.ImMessageCommandModel, error) { + return publishMessageCommand(ctx, in, "private", consts.ImAdminCommandMessageHide) +} + +func (s *sSysImPrivateMessage) Delete(ctx context.Context, in *sysin.ImMessageActionInp) (*sysin.ImMessageCommandModel, error) { + return publishMessageCommand(ctx, in, "private", consts.ImAdminCommandMessageDelete) +} + +func (s *sSysImGroupMessage) Hide(ctx context.Context, in *sysin.ImMessageActionInp) (*sysin.ImMessageCommandModel, error) { + return publishMessageCommand(ctx, in, "group", consts.ImAdminCommandMessageHide) +} + +func (s *sSysImGroupMessage) Delete(ctx context.Context, in *sysin.ImMessageActionInp) (*sysin.ImMessageCommandModel, error) { + return publishMessageCommand(ctx, in, "group", consts.ImAdminCommandMessageDelete) +} + +func publishMessageCommand(ctx context.Context, in *sysin.ImMessageActionInp, conversationType string, eventType string) (*sysin.ImMessageCommandModel, error) { + if err := in.Filter(ctx); err != nil { + return nil, err + } + tenant, err := resolveImAdminTenant(ctx, in.TenantId) + if err != nil { + return nil, err + } + user := contexts.GetUser(ctx) + actorName := "" + if user != nil { + actorName = user.RealName + } + bizId := in.Topic + ":" + strconv.FormatInt(in.SeqId, 10) + command, err := imkafka.PublishAdminCommand(ctx, imkafka.AdminCommandInput{ + TenantID: tenant.Id, TenantCode: tenant.Code, EventType: eventType, BizType: "message", BizID: bizId, + Actor: &imkafka.Actor{Type: "admin", ID: strconv.FormatInt(contexts.GetUserId(ctx), 10), DisplayName: actorName}, + Payload: map[string]any{"conversation_type": conversationType, "topic": in.Topic, "seq_id": in.SeqId, "reason": in.Reason}, + CreatedBy: contexts.GetUserId(ctx), + }) + result := "success" + if err != nil { + result = "failed" + } + _ = writeImAdminAudit(ctx, tenant.Id, eventType, "message", in.SeqId, result, map[string]any{"conversation_type": conversationType, "topic": in.Topic, "reason": in.Reason}) + if err != nil { + return nil, err + } + return &sysin.ImMessageCommandModel{CommandId: command.CommandID, EventId: command.EventID, Status: command.Status}, nil +} + +func (s *sSysImPrivateMessage) Export(ctx context.Context, in *sysin.ImPrivateMessageListInp) error { + list, total, err := s.List(ctx, in) + if err != nil { + return err + } + return exportImMessages(ctx, list, total, in.Page, in.PerPage, "单聊消息") +} + +func (s *sSysImGroupMessage) Export(ctx context.Context, in *sysin.ImGroupMessageListInp) error { + list, total, err := s.List(ctx, in) + if err != nil { + return err + } + return exportImMessages(ctx, list, total, in.Page, in.PerPage, "群聊消息") +} + +func exportImMessages(ctx context.Context, list []*sysin.ImMessageListModel, total int, page int, perPage int, label string) error { + tags, err := convert.GetEntityDescTags(sysin.ImMessageExportModel{}) + if err != nil { + return err + } + var exports []sysin.ImMessageExportModel + if err = gconv.Scan(list, &exports); err != nil { + return err + } + fileName := "导出" + label + "-" + gctx.CtxId(ctx) + sheetName := fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", total, form.CalPageCount(total, perPage), page, len(list)) + return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName) +} + +func setQuery(values url.Values, key string, value string) { + if value != "" { + values.Set(key, value) + } +} diff --git a/server/internal/logic/sys/im_policy_support.go b/server/internal/logic/sys/im_policy_support.go new file mode 100644 index 0000000..ab6c025 --- /dev/null +++ b/server/internal/logic/sys/im_policy_support.go @@ -0,0 +1,60 @@ +package sys + +import ( + "context" + "fmt" + "strconv" + + "hotgo/internal/dao" + "hotgo/internal/library/contexts" + "hotgo/internal/library/imkafka" + "hotgo/internal/library/imtenant" + "hotgo/internal/model/entity" + "hotgo/internal/service" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" +) + +func resolveImPolicyTenant(ctx context.Context, selectedTenantID int64) (tenantID int64, tenantCode string, err error) { + operatorID := contexts.GetUserId(ctx) + if service.AdminMember().VerifySuperId(ctx, operatorID) && selectedTenantID == 0 { + return 0, "*", nil + } + scope, err := resolveImAdminScope(ctx, selectedTenantID) + if err != nil { + return 0, "", err + } + var tenant *entity.ImTenant + if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx). + Where(dao.ImTenant.Columns().Id, scope.TenantId).Scan(&tenant); err != nil { + return 0, "", gerror.Wrap(err, "获取IM租户失败") + } + if tenant == nil { + return 0, "", gerror.New("IM租户不存在") + } + return tenant.Id, tenant.Code, nil +} + +func policyJSON(value *gjson.Json, fallback string) string { + if value == nil || value.String() == "" || value.String() == "null" { + return fallback + } + return value.String() +} + +func publishImPolicyEvent(ctx context.Context, tenantID int64, tenantCode, eventType, group string, version uint) error { + return imkafka.PublishConfigEvent(ctx, imkafka.ConfigEventInput{ + TenantID: tenantID, + TenantCode: tenantCode, + EventType: eventType, + Group: group, + Keys: []string{"version", "payload"}, + BizID: fmt.Sprintf("%s:%d:%d", group, tenantID, version), + Actor: &imkafka.Actor{ + Type: "admin", + ID: strconv.FormatInt(contexts.GetUserId(ctx), 10), + }, + CreatedBy: contexts.GetUserId(ctx), + }) +} diff --git a/server/internal/logic/sys/im_security_policy.go b/server/internal/logic/sys/im_security_policy.go new file mode 100644 index 0000000..eec3393 --- /dev/null +++ b/server/internal/logic/sys/im_security_policy.go @@ -0,0 +1,331 @@ +package sys + +import ( + "context" + "net" + + "hotgo/internal/consts" + "hotgo/internal/dao" + "hotgo/internal/library/contexts" + "hotgo/internal/library/imtenant" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type sSysImSecurityPolicy struct{} + +func NewSysImSecurityPolicy() *sSysImSecurityPolicy { return &sSysImSecurityPolicy{} } +func init() { service.RegisterSysImSecurityPolicy(NewSysImSecurityPolicy()) } + +func securityPolicyFields(alias string) []any { + c := dao.ImSecurityPolicy.Columns() + p := alias + if p != "" { + p += "." + } + return []any{p + c.Id, p + c.TenantId, p + c.PolicyVersion, p + c.PolicyName, p + c.PublishStatus, "CAST(" + p + c.PasswordPolicy + " AS CHAR) password_policy", "CAST(" + p + c.LoginRateLimit + " AS CHAR) login_rate_limit", "CAST(" + p + c.SensitiveWordPolicy + " AS CHAR) sensitive_word_policy", "CAST(" + p + c.FileReviewPolicy + " AS CHAR) file_review_policy", "CAST(" + p + c.RealnamePolicy + " AS CHAR) realname_policy", "CAST(" + p + c.RiskChallengePolicy + " AS CHAR) risk_challenge_policy", p + c.GlobalFriendEnabled, "CAST(" + p + c.FriendDisabledPolicy + " AS CHAR) friend_disabled_policy", p + c.MessageCurfewEnabled, p + c.Remark, p + c.PublishedAt, p + c.PublishedBy, p + c.CreatedAt, p + c.UpdatedAt} +} + +func (s *sSysImSecurityPolicy) List(ctx context.Context, in *sysin.ImSecurityPolicyListInp) (list []*sysin.ImSecurityPolicyModel, total int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return nil, 0, err + } + c := dao.ImSecurityPolicy.Columns() + model := imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Fields(securityPolicyFields("")...).Where(c.TenantId, tenantID).WhereNull(c.DeletedAt) + if in.PublishStatus != "" { + model = model.Where(c.PublishStatus, in.PublishStatus) + } + err = model.Page(in.Page, in.PerPage).OrderDesc(c.PolicyVersion).ScanAndCount(&list, &total, false) + return +} + +func (s *sSysImSecurityPolicy) View(ctx context.Context, in *sysin.ImSecurityPolicyViewInp) (res *sysin.ImSecurityPolicyModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + c := dao.ImSecurityPolicy.Columns() + baseModel := imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Fields(securityPolicyFields("")...).WhereNull(c.DeletedAt) + if in.Id > 0 { + err = baseModel.Where(c.Id, in.Id).Scan(&res) + } else { + tenantID, _, resolveErr := resolveImPolicyTenant(ctx, in.TenantId) + if resolveErr != nil { + return nil, resolveErr + } + err = baseModel.Where(c.TenantId, tenantID). + Where(c.PublishStatus, consts.ImSecurityPublishPublished). + OrderDesc(c.PolicyVersion).Scan(&res) + if err == nil && res == nil { + err = baseModel.Where(c.TenantId, tenantID).OrderDesc(c.PolicyVersion).Scan(&res) + } + } + if err != nil { + return nil, gerror.Wrap(err, "获取安全策略失败") + } + if res == nil { + return nil, gerror.New("安全策略不存在") + } + if _, _, err = resolveImPolicyTenant(ctx, res.TenantId); err != nil { + return nil, err + } + res.CurfewRules, err = s.curfewRules(ctx, res.Id) + return +} + +func (s *sSysImSecurityPolicy) curfewRules(ctx context.Context, policyID int64) (list []*sysin.ImMessageCurfewRuleModel, err error) { + c := dao.ImMessageCurfewRules.Columns() + err = imtenant.RegistryDB().Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Fields(c.Id, c.RuleCode, c.RuleName, c.DateType, "CAST("+c.CustomDates+" AS CHAR) custom_dates", "CAST("+c.Weekdays+" AS CHAR) weekdays", c.Timezone, "TIME_FORMAT("+c.StartTime+",'%H:%i') start_time", "TIME_FORMAT("+c.EndTime+",'%H:%i') end_time", c.Scope, "CAST("+c.WhitelistUserIds+" AS CHAR) whitelist_user_ids", "CAST("+c.WhitelistGroupIds+" AS CHAR) whitelist_group_ids", c.Message, c.Status, c.Sort).Where(c.PolicyId, policyID).WhereNull(c.DeletedAt).OrderAsc(c.Sort).OrderAsc(c.Id).Scan(&list) + return +} + +func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPolicySaveInp) (id int64, err error) { + if err = in.Filter(ctx); err != nil { + return + } + tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return 0, err + } + db := imtenant.RegistryDB() + c := dao.ImSecurityPolicy.Columns() + rc := dao.ImMessageCurfewRules.Columns() + operator := contexts.GetUserId(ctx) + now := gtime.Now() + err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + version := uint(0) + if in.Id > 0 { + current, readErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One() + if readErr != nil { + return readErr + } + if current.IsEmpty() || current[c.TenantId].Int64() != tenantID { + return gerror.New("安全策略不存在") + } + if current[c.PublishStatus].String() != consts.ImSecurityPublishDraft { + return gerror.New("只有草稿状态的安全策略可以编辑") + } + version = current[c.PolicyVersion].Uint() + id = in.Id + } else { + maxValue, maxErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.TenantId, tenantID).Max(c.PolicyVersion) + if maxErr != nil { + return maxErr + } + version = uint(maxValue) + 1 + } + data := g.Map{c.TenantId: tenantID, c.PolicyVersion: version, c.PolicyName: in.PolicyName, c.PublishStatus: consts.ImSecurityPublishDraft, c.PasswordPolicy: policyJSON(in.PasswordPolicy, "{}"), c.LoginRateLimit: policyJSON(in.LoginRateLimit, "{}"), c.SensitiveWordPolicy: policyJSON(in.SensitiveWordPolicy, "{}"), c.FileReviewPolicy: policyJSON(in.FileReviewPolicy, "{}"), c.RealnamePolicy: policyJSON(in.RealnamePolicy, "{}"), c.RiskChallengePolicy: policyJSON(in.RiskChallengePolicy, "{}"), c.GlobalFriendEnabled: in.GlobalFriendEnabled, c.FriendDisabledPolicy: policyJSON(in.FriendDisabledPolicy, `{"hide_profile":true,"block_qrcode":true,"block_group_member_entry":true,"blocked_entries":["search","profile_card","group_member","qrcode","invite_auto_add","recommendation"],"message":"当前时段暂不允许添加好友"}`), c.MessageCurfewEnabled: in.MessageCurfewEnabled, c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: operator} + if in.Id > 0 { + if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update(); updateErr != nil { + return updateErr + } + } else { + data[c.CreatedAt] = now + data[c.CreatedBy] = operator + result, insertErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Data(data).Insert() + if insertErr != nil { + return insertErr + } + id, insertErr = result.LastInsertId() + if insertErr != nil { + return insertErr + } + } + if _, deleteErr := tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Where(rc.PolicyId, id).Delete(); deleteErr != nil { + return deleteErr + } + for _, rule := range in.CurfewRules { + _, insertErr := tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Data(g.Map{rc.TenantId: tenantID, rc.PolicyId: id, rc.RuleCode: rule.RuleCode, rc.RuleName: rule.RuleName, rc.DateType: rule.DateType, rc.CustomDates: policyJSON(rule.CustomDates, "[]"), rc.Weekdays: policyJSON(rule.Weekdays, "[]"), rc.Timezone: rule.Timezone, rc.StartTime: rule.StartTime, rc.EndTime: rule.EndTime, rc.Scope: rule.Scope, rc.WhitelistUserIds: policyJSON(rule.WhitelistUserIds, "[]"), rc.WhitelistGroupIds: policyJSON(rule.WhitelistGroupIds, "[]"), rc.Message: rule.Message, rc.Status: rule.Status, rc.Sort: rule.Sort, rc.CreatedAt: now, rc.CreatedBy: operator, rc.UpdatedAt: now, rc.UpdatedBy: operator}).Insert() + if insertErr != nil { + return insertErr + } + } + return nil + }) + if err != nil { + return 0, gerror.Wrap(err, "保存安全策略草稿失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_security_policy_save", "im_security_policy", id, "success", g.Map{"global_friend_enabled": in.GlobalFriendEnabled, "message_curfew_enabled": in.MessageCurfewEnabled}) + return id, nil +} + +func (s *sSysImSecurityPolicy) Publish(ctx context.Context, in *sysin.ImSecurityPolicyPublishInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + db := imtenant.RegistryDB() + c := dao.ImSecurityPolicy.Columns() + var row gdb.Record + var err error + if row, err = db.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil { + return err + } + if row.IsEmpty() { + return gerror.New("安全策略不存在") + } + if row[c.PublishStatus].String() != consts.ImSecurityPublishDraft { + return gerror.New("只有草稿状态可以发布") + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, row[c.TenantId].Int64()) + if err != nil { + return err + } + operator := contexts.GetUserId(ctx) + now := gtime.Now() + err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.TenantId, tenantID).Where(c.PublishStatus, consts.ImSecurityPublishPublished).WhereNot(c.Id, in.Id).WhereNull(c.DeletedAt).Data(g.Map{c.PublishStatus: consts.ImSecurityPublishArchived, c.UpdatedAt: now, c.UpdatedBy: operator}).Update(); updateErr != nil { + return updateErr + } + if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(g.Map{c.PublishStatus: consts.ImSecurityPublishPublished, c.PublishedAt: now, c.PublishedBy: operator, c.UpdatedAt: now, c.UpdatedBy: operator}).Update(); updateErr != nil { + return updateErr + } + if tenantID > 0 { + tc := dao.ImTenantConfig.Columns() + if _, updateErr := tx.Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).Where(tc.TenantId, tenantID).Data(g.Map{tc.SecurityPolicyVersion: row[c.PolicyVersion].Uint(), tc.GlobalFriendEnabled: row[c.GlobalFriendEnabled].Int(), tc.MessageCurfewEnabled: row[c.MessageCurfewEnabled].Int(), tc.UpdatedAt: now, tc.UpdatedBy: operator}).Update(); updateErr != nil { + return updateErr + } + } + return nil + }) + if err != nil { + return gerror.Wrap(err, "发布安全策略失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_security_policy_publish", "im_security_policy", in.Id, "success", g.Map{"reason": in.Reason, "version": row[c.PolicyVersion].Uint()}) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", row[c.PolicyVersion].Uint()) +} + +func (s *sSysImSecurityPolicy) RestoreDefault(ctx context.Context, in *sysin.ImSecurityPolicyRestoreInp) (int64, error) { + if err := in.Filter(ctx); err != nil { + return 0, err + } + tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return 0, err + } + c := dao.ImSecurityPolicy.Columns() + var source *sysin.ImSecurityPolicyModel + err = imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx). + Fields(securityPolicyFields("")...).Where(c.TenantId, 0). + Where(c.PublishStatus, consts.ImSecurityPublishPublished).WhereNull(c.DeletedAt). + OrderDesc(c.PolicyVersion).Scan(&source) + if err != nil { + return 0, gerror.Wrap(err, "读取全局默认安全策略失败") + } + if source == nil { + return 0, gerror.New("全局默认安全策略不存在") + } + source.CurfewRules, err = s.curfewRules(ctx, source.Id) + if err != nil { + return 0, err + } + rules := make([]*sysin.ImMessageCurfewRuleInp, 0, len(source.CurfewRules)) + for _, rule := range source.CurfewRules { + rules = append(rules, &sysin.ImMessageCurfewRuleInp{RuleCode: rule.RuleCode, RuleName: rule.RuleName, DateType: rule.DateType, CustomDates: gjson.New(rule.CustomDates), Weekdays: gjson.New(rule.Weekdays), Timezone: rule.Timezone, StartTime: rule.StartTime, EndTime: rule.EndTime, Scope: rule.Scope, WhitelistUserIds: gjson.New(rule.WhitelistUserIds), WhitelistGroupIds: gjson.New(rule.WhitelistGroupIds), Message: rule.Message, Status: rule.Status, Sort: rule.Sort}) + } + id, err := s.Save(ctx, &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: "恢复默认安全策略", PasswordPolicy: gjson.New(source.PasswordPolicy), LoginRateLimit: gjson.New(source.LoginRateLimit), SensitiveWordPolicy: gjson.New(source.SensitiveWordPolicy), FileReviewPolicy: gjson.New(source.FileReviewPolicy), RealnamePolicy: gjson.New(source.RealnamePolicy), RiskChallengePolicy: gjson.New(source.RiskChallengePolicy), GlobalFriendEnabled: source.GlobalFriendEnabled, FriendDisabledPolicy: gjson.New(source.FriendDisabledPolicy), MessageCurfewEnabled: source.MessageCurfewEnabled, Remark: in.Reason, CurfewRules: rules}) + if err == nil { + _ = writeImAdminAudit(ctx, tenantID, "im_security_policy_restore_default", "im_security_policy", id, "success", g.Map{"reason": in.Reason}) + } + return id, err +} + +func (s *sSysImSecurityPolicy) IPRuleList(ctx context.Context, in *sysin.ImSecurityIPRuleListInp) (list []*sysin.ImSecurityIPRuleModel, total int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return nil, 0, err + } + c := dao.ImSecurityIpRules.Columns() + model := imtenant.RegistryDB().Model(dao.ImSecurityIpRules.Table()).Safe().Ctx(ctx).Fields(c.Id, c.TenantId, c.RuleType, c.IpCidr, c.Action, "CAST("+c.SceneJson+" AS CHAR) scene_json", c.RiskLevel, c.Status, c.ExpiresAt, c.Remark, c.CreatedAt, c.UpdatedAt).Where(c.TenantId, tenantID).WhereNull(c.DeletedAt) + if in.RuleType != "" { + model = model.Where(c.RuleType, in.RuleType) + } + if in.IpCidr != "" { + model = model.WhereLike(c.IpCidr, "%"+in.IpCidr+"%") + } + if in.Status != 0 { + model = model.Where(c.Status, in.Status) + } + err = model.Page(in.Page, in.PerPage).OrderDesc(c.Id).ScanAndCount(&list, &total, false) + return +} + +func (s *sSysImSecurityPolicy) IPRuleEdit(ctx context.Context, in *sysin.ImSecurityIPRuleEditInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + if net.ParseIP(in.IpCidr) == nil { + if _, _, err := net.ParseCIDR(in.IpCidr); err != nil { + return gerror.New("IP或CIDR格式不合法") + } + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return err + } + c := dao.ImSecurityIpRules.Columns() + db := imtenant.RegistryDB() + now := gtime.Now() + data := g.Map{c.TenantId: tenantID, c.RuleType: in.RuleType, c.IpCidr: in.IpCidr, c.Action: in.Action, c.SceneJson: policyJSON(in.SceneJson, "[]"), c.RiskLevel: in.RiskLevel, c.Status: in.Status, c.ExpiresAt: in.ExpiresAt, c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)} + targetID := in.Id + if in.Id > 0 { + var row gdb.Record + if row, err = db.Model(dao.ImSecurityIpRules.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil { + return err + } + if row.IsEmpty() || row[c.TenantId].Int64() != tenantID { + return gerror.New("IP规则不存在") + } + _, err = db.Model(dao.ImSecurityIpRules.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update() + } else { + data[c.CreatedAt] = now + data[c.CreatedBy] = contexts.GetUserId(ctx) + result, insertErr := db.Model(dao.ImSecurityIpRules.Table()).Safe().Ctx(ctx).Data(data).Insert() + err = insertErr + if err == nil { + targetID, err = result.LastInsertId() + } + } + if err != nil { + return gerror.Wrap(err, "保存IP规则失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_security_ip_rule_edit", "im_security_ip_rule", targetID, "success", g.Map{"ip_cidr": in.IpCidr, "rule_type": in.RuleType}) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", uint(targetID)) +} + +func (s *sSysImSecurityPolicy) IPRuleDelete(ctx context.Context, in *sysin.ImSecurityIPRuleDeleteInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + c := dao.ImSecurityIpRules.Columns() + db := imtenant.RegistryDB() + var row gdb.Record + var err error + if row, err = db.Model(dao.ImSecurityIpRules.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil { + return err + } + if row.IsEmpty() { + return gerror.New("IP规则不存在") + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, row[c.TenantId].Int64()) + if err != nil { + return err + } + _, err = db.Model(dao.ImSecurityIpRules.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(g.Map{c.Status: consts.StatusDisable, c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx), c.UpdatedAt: gtime.Now(), c.UpdatedBy: contexts.GetUserId(ctx)}).Update() + if err != nil { + return gerror.Wrap(err, "删除IP规则失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_security_ip_rule_delete", "im_security_ip_rule", in.Id, "success", nil) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", uint(in.Id)) +} diff --git a/server/internal/logic/sys/im_sensitive_word.go b/server/internal/logic/sys/im_sensitive_word.go new file mode 100644 index 0000000..12780f0 --- /dev/null +++ b/server/internal/logic/sys/im_sensitive_word.go @@ -0,0 +1,266 @@ +package sys + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "strings" + + "hotgo/internal/consts" + "hotgo/internal/dao" + "hotgo/internal/library/contexts" + "hotgo/internal/library/imtenant" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type sSysImSensitiveWord struct{} + +func NewSysImSensitiveWord() *sSysImSensitiveWord { return &sSysImSensitiveWord{} } +func init() { service.RegisterSysImSensitiveWord(NewSysImSensitiveWord()) } + +func normalizedSensitiveWord(value string) (string, string) { + normalized := strings.ToLower(strings.TrimSpace(value)) + sum := sha256.Sum256([]byte(normalized)) + return normalized, hex.EncodeToString(sum[:]) +} + +func (s *sSysImSensitiveWord) List(ctx context.Context, in *sysin.ImSensitiveWordListInp) (list []*sysin.ImSensitiveWordListModel, total int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return nil, 0, err + } + c := dao.ImSensitiveWords.Columns() + gc := dao.ImSensitiveWordGroups.Columns() + model := imtenant.RegistryDB().Model(dao.ImSensitiveWords.Table()+" w").Safe().Ctx(ctx).LeftJoin(dao.ImSensitiveWordGroups.Table()+" g", "g."+gc.Id+"=w."+c.GroupId).Fields("w."+c.Id, "w."+c.TenantId, "w."+c.GroupId, "g."+gc.GroupName, "w."+c.Word, "w."+c.MatchType, "CAST(w."+c.SceneJson+" AS CHAR) scene_json", "w."+c.Action, "w."+c.Replacement, "w."+c.RiskLevel, "w."+c.Priority, "w."+c.Status, "w."+c.Remark, "w."+c.CreatedAt, "w."+c.UpdatedAt).Where("w."+c.TenantId, tenantID).WhereNull("w." + c.DeletedAt) + if in.GroupId > 0 { + model = model.Where("w."+c.GroupId, in.GroupId) + } + if in.Word != "" { + model = model.WhereLike("w."+c.Word, "%"+in.Word+"%") + } + if in.MatchType != "" { + model = model.Where("w."+c.MatchType, in.MatchType) + } + if in.Action != "" { + model = model.Where("w."+c.Action, in.Action) + } + if in.RiskLevel != "" { + model = model.Where("w."+c.RiskLevel, in.RiskLevel) + } + if in.Status != 0 { + model = model.Where("w."+c.Status, in.Status) + } + err = model.Page(in.Page, in.PerPage).OrderDesc("w."+c.Priority).OrderDesc("w."+c.Id).ScanAndCount(&list, &total, false) + if err != nil { + err = gerror.Wrap(err, "获取敏感词列表失败") + } + return +} + +func (s *sSysImSensitiveWord) Edit(ctx context.Context, in *sysin.ImSensitiveWordEditInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return err + } + if err = validateSensitiveWordGroup(ctx, tenantID, in.GroupId); err != nil { + return err + } + _, hash := normalizedSensitiveWord(in.Word) + c := dao.ImSensitiveWords.Columns() + db := imtenant.RegistryDB() + now := gtime.Now() + data := g.Map{c.TenantId: tenantID, c.GroupId: in.GroupId, c.Word: in.Word, c.WordHash: hash, c.MatchType: in.MatchType, c.SceneJson: policyJSON(in.SceneJson, "[]"), c.Action: in.Action, c.Replacement: in.Replacement, c.RiskLevel: in.RiskLevel, c.Priority: in.Priority, c.Status: in.Status, c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)} + targetID := in.Id + if in.Id > 0 { + var row gdb.Record + if row, err = db.Model(dao.ImSensitiveWords.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil { + return err + } + if row.IsEmpty() || row[c.TenantId].Int64() != tenantID { + return gerror.New("敏感词不存在") + } + _, err = db.Model(dao.ImSensitiveWords.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update() + } else { + data[c.CreatedAt] = now + data[c.CreatedBy] = contexts.GetUserId(ctx) + result, insertErr := db.Model(dao.ImSensitiveWords.Table()).Safe().Ctx(ctx).Data(data).Insert() + err = insertErr + if err == nil { + targetID, err = result.LastInsertId() + } + } + if err != nil { + return gerror.Wrap(err, "保存敏感词失败,请检查是否重复") + } + _ = writeImAdminAudit(ctx, tenantID, "im_sensitive_word_edit", "im_sensitive_word", targetID, "success", g.Map{"group_id": in.GroupId, "action": in.Action}) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "sensitive_words.updated", "sensitive_words", uint(targetID)) +} + +func (s *sSysImSensitiveWord) Delete(ctx context.Context, in *sysin.ImSensitiveWordDeleteInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + return s.updateWordState(ctx, in.Id, consts.StatusDisable, true, "im_sensitive_word_delete") +} +func (s *sSysImSensitiveWord) Status(ctx context.Context, in *sysin.ImSensitiveWordStatusInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + return s.updateWordState(ctx, in.Id, in.Status, false, "im_sensitive_word_status") +} +func (s *sSysImSensitiveWord) updateWordState(ctx context.Context, id int64, status int, deleted bool, action string) error { + c := dao.ImSensitiveWords.Columns() + db := imtenant.RegistryDB() + var row gdb.Record + var err error + if row, err = db.Model(dao.ImSensitiveWords.Table()).Safe().Ctx(ctx).Where(c.Id, id).WhereNull(c.DeletedAt).One(); err != nil { + return err + } + if row.IsEmpty() { + return gerror.New("敏感词不存在") + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, row[c.TenantId].Int64()) + if err != nil { + return err + } + data := g.Map{c.Status: status, c.UpdatedAt: gtime.Now(), c.UpdatedBy: contexts.GetUserId(ctx)} + if deleted { + data[c.DeletedAt] = gtime.Now() + data[c.DeletedBy] = contexts.GetUserId(ctx) + } + if _, err = db.Model(dao.ImSensitiveWords.Table()).Safe().Ctx(ctx).Where(c.Id, id).Data(data).Update(); err != nil { + return gerror.Wrap(err, "更新敏感词失败") + } + _ = writeImAdminAudit(ctx, tenantID, action, "im_sensitive_word", id, "success", g.Map{"status": status}) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "sensitive_words.updated", "sensitive_words", uint(id)) +} + +func (s *sSysImSensitiveWord) BatchImport(ctx context.Context, in *sysin.ImSensitiveWordBatchImportInp) (res *sysin.ImSensitiveWordBatchImportModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return nil, err + } + if err = validateSensitiveWordGroup(ctx, tenantID, in.GroupId); err != nil { + return nil, err + } + res = &sysin.ImSensitiveWordBatchImportModel{} + db := imtenant.RegistryDB() + c := dao.ImSensitiveWords.Columns() + seen := map[string]struct{}{} + err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + for _, raw := range in.Words { + word, hash := normalizedSensitiveWord(raw) + if word == "" { + res.Skipped++ + continue + } + if _, ok := seen[hash]; ok { + res.Skipped++ + continue + } + seen[hash] = struct{}{} + count, countErr := tx.Model(dao.ImSensitiveWords.Table()).Safe().Ctx(ctx).Where(c.TenantId, tenantID).Where(c.GroupId, in.GroupId).Where(c.WordHash, hash).WhereNull(c.DeletedAt).Count() + if countErr != nil { + return countErr + } + if count > 0 { + res.Skipped++ + continue + } + now := gtime.Now() + _, insertErr := tx.Model(dao.ImSensitiveWords.Table()).Safe().Ctx(ctx).Data(g.Map{c.TenantId: tenantID, c.GroupId: in.GroupId, c.Word: strings.TrimSpace(raw), c.WordHash: hash, c.MatchType: in.MatchType, c.SceneJson: policyJSON(in.SceneJson, "[]"), c.Action: in.Action, c.Replacement: in.Replacement, c.RiskLevel: in.RiskLevel, c.Status: consts.StatusEnabled, c.CreatedAt: now, c.CreatedBy: contexts.GetUserId(ctx), c.UpdatedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)}).Insert() + if insertErr != nil { + return insertErr + } + res.Inserted++ + } + return nil + }) + if err != nil { + return nil, gerror.Wrap(err, "批量导入敏感词失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_sensitive_word_batch_import", "im_sensitive_word", 0, "success", g.Map{"inserted": res.Inserted, "skipped": res.Skipped}) + err = publishImPolicyEvent(ctx, tenantID, tenantCode, "sensitive_words.updated", "sensitive_words", uint(res.Inserted)) + return +} + +func (s *sSysImSensitiveWord) GroupList(ctx context.Context, in *sysin.ImSensitiveWordGroupListInp) (list []*sysin.ImSensitiveWordGroupModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return nil, err + } + c := dao.ImSensitiveWordGroups.Columns() + err = imtenant.RegistryDB().Model(dao.ImSensitiveWordGroups.Table()).Safe().Ctx(ctx).Fields(c.Id, c.TenantId, c.GroupCode, c.GroupName, c.Priority, c.Status, c.Remark, c.CreatedAt).Where(c.TenantId, tenantID).WhereNull(c.DeletedAt).OrderDesc(c.Priority).OrderAsc(c.GroupName).Scan(&list) + return +} + +func (s *sSysImSensitiveWord) GroupEdit(ctx context.Context, in *sysin.ImSensitiveWordGroupEditInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return err + } + c := dao.ImSensitiveWordGroups.Columns() + db := imtenant.RegistryDB() + now := gtime.Now() + data := g.Map{c.TenantId: tenantID, c.GroupCode: in.GroupCode, c.GroupName: in.GroupName, c.Priority: in.Priority, c.Status: in.Status, c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)} + targetID := in.Id + if in.Id > 0 { + var row gdb.Record + if row, err = db.Model(dao.ImSensitiveWordGroups.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil { + return err + } + if row.IsEmpty() || row[c.TenantId].Int64() != tenantID { + return gerror.New("敏感词分组不存在") + } + if row[c.GroupCode].String() != in.GroupCode { + return gerror.New("分组编码创建后不能修改") + } + _, err = db.Model(dao.ImSensitiveWordGroups.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update() + } else { + data[c.CreatedAt] = now + data[c.CreatedBy] = contexts.GetUserId(ctx) + result, insertErr := db.Model(dao.ImSensitiveWordGroups.Table()).Safe().Ctx(ctx).Data(data).Insert() + err = insertErr + if err == nil { + targetID, err = result.LastInsertId() + } + } + if err != nil { + return gerror.Wrap(err, "保存敏感词分组失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_sensitive_word_group_edit", "im_sensitive_word_group", targetID, "success", nil) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "sensitive_words.updated", "sensitive_words", uint(targetID)) +} + +func validateSensitiveWordGroup(ctx context.Context, tenantID, groupID int64) error { + c := dao.ImSensitiveWordGroups.Columns() + count, err := imtenant.RegistryDB().Model(dao.ImSensitiveWordGroups.Table()).Safe().Ctx(ctx).Where(c.Id, groupID).Where(c.TenantId, tenantID).WhereNull(c.DeletedAt).Count() + if err != nil { + return gerror.Wrap(err, "检查敏感词分组失败") + } + if count == 0 { + return gerror.New("敏感词分组不存在") + } + return nil +} diff --git a/server/internal/logic/sys/im_user_policy_group.go b/server/internal/logic/sys/im_user_policy_group.go new file mode 100644 index 0000000..e1daf71 --- /dev/null +++ b/server/internal/logic/sys/im_user_policy_group.go @@ -0,0 +1,290 @@ +package sys + +import ( + "context" + + "hotgo/internal/consts" + "hotgo/internal/dao" + "hotgo/internal/library/contexts" + "hotgo/internal/library/imtenant" + "hotgo/internal/model/input/sysin" + "hotgo/internal/service" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type sSysImUserPolicyGroup struct{} + +func NewSysImUserPolicyGroup() *sSysImUserPolicyGroup { return &sSysImUserPolicyGroup{} } +func init() { service.RegisterSysImUserPolicyGroup(NewSysImUserPolicyGroup()) } + +func (s *sSysImUserPolicyGroup) List(ctx context.Context, in *sysin.ImUserPolicyGroupListInp) (list []*sysin.ImUserPolicyGroupListModel, total int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return nil, 0, err + } + c := dao.ImUserPolicyGroups.Columns() + mc := dao.ImUserPolicyGroupMembers.Columns() + model := imtenant.RegistryDB().Model(dao.ImUserPolicyGroups.Table()+" p").Safe().Ctx(ctx). + Fields("p."+c.Id, "p."+c.TenantId, "p."+c.GroupCode, "p."+c.GroupName, "p."+c.Priority, "p."+c.Status, + "CAST(p."+c.PolicyJson+" AS CHAR) policy_json", "CAST(p."+c.IpWhitelist+" AS CHAR) ip_whitelist", + "CAST(p."+c.IpBlacklist+" AS CHAR) ip_blacklist", "p."+c.Version, "p."+c.Remark, "p."+c.CreatedAt, "p."+c.UpdatedAt, + "(SELECT COUNT(*) FROM "+dao.ImUserPolicyGroupMembers.Table()+" m WHERE m."+mc.GroupId+"=p."+c.Id+" AND m."+mc.Status+"=1 AND m."+mc.DeletedAt+" IS NULL) member_count"). + Where("p."+c.TenantId, tenantID).WhereNull("p." + c.DeletedAt) + if in.Keyword != "" { + keyword := "%" + in.Keyword + "%" + model = model.Where("(p."+c.GroupCode+" LIKE ? OR p."+c.GroupName+" LIKE ?)", keyword, keyword) + } + if in.Status != 0 { + model = model.Where("p."+c.Status, in.Status) + } + err = model.Page(in.Page, in.PerPage).OrderDesc("p."+c.Priority).OrderDesc("p."+c.Id).ScanAndCount(&list, &total, false) + if err != nil { + err = gerror.Wrap(err, "获取用户策略分组失败") + } + return +} + +func (s *sSysImUserPolicyGroup) View(ctx context.Context, in *sysin.ImUserPolicyGroupViewInp) (res *sysin.ImUserPolicyGroupViewModel, err error) { + if err = in.Filter(ctx); err != nil { + return + } + c := dao.ImUserPolicyGroups.Columns() + var row *sysin.ImUserPolicyGroupListModel + err = imtenant.RegistryDB().Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx). + Fields(c.Id, c.TenantId, c.GroupCode, c.GroupName, c.Priority, c.Status, "CAST("+c.PolicyJson+" AS CHAR) policy_json", "CAST("+c.IpWhitelist+" AS CHAR) ip_whitelist", "CAST("+c.IpBlacklist+" AS CHAR) ip_blacklist", c.Version, c.Remark, c.CreatedAt, c.UpdatedAt). + Where(c.Id, in.Id).WhereNull(c.DeletedAt).Scan(&row) + if err != nil { + return nil, gerror.Wrap(err, "获取用户策略分组详情失败") + } + if row == nil { + return nil, gerror.New("用户策略分组不存在") + } + if _, _, err = resolveImPolicyTenant(ctx, row.TenantId); err != nil { + return nil, err + } + return &sysin.ImUserPolicyGroupViewModel{ImUserPolicyGroupListModel: *row}, nil +} + +func (s *sSysImUserPolicyGroup) Edit(ctx context.Context, in *sysin.ImUserPolicyGroupEditInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, in.TenantId) + if err != nil { + return err + } + c := dao.ImUserPolicyGroups.Columns() + db := imtenant.RegistryDB() + now := gtime.Now() + operator := contexts.GetUserId(ctx) + data := g.Map{c.GroupName: in.GroupName, c.Priority: in.Priority, c.Status: in.Status, c.PolicyJson: policyJSON(in.PolicyJson, "{}"), c.IpWhitelist: policyJSON(in.IpWhitelist, "[]"), c.IpBlacklist: policyJSON(in.IpBlacklist, "[]"), c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: operator} + var targetID int64 + if in.Id > 0 { + var current gdb.Record + if current, err = db.Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil { + return gerror.Wrap(err, "读取用户策略分组失败") + } + if current.IsEmpty() || current[c.TenantId].Int64() != tenantID { + return gerror.New("用户策略分组不存在") + } + if current[c.GroupCode].String() != in.GroupCode { + return gerror.New("分组编码创建后不能修改") + } + data[c.Version] = gdb.Raw("version+1") + if _, err = db.Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update(); err != nil { + return gerror.Wrap(err, "更新用户策略分组失败") + } + targetID = in.Id + } else { + data[c.TenantId] = tenantID + data[c.GroupCode] = in.GroupCode + data[c.Version] = 1 + data[c.CreatedAt] = now + data[c.CreatedBy] = operator + result, insertErr := db.Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Data(data).Insert() + if insertErr != nil { + return gerror.Wrap(insertErr, "新增用户策略分组失败") + } + targetID, err = result.LastInsertId() + if err != nil { + return gerror.Wrap(err, "获取用户策略分组ID失败") + } + } + _ = writeImAdminAudit(ctx, tenantID, "im_user_policy_group_edit", "im_user_policy_group", targetID, "success", g.Map{"group_code": in.GroupCode}) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "user_policy_group.updated", "user_policy_groups", uint(targetID)) +} + +func (s *sSysImUserPolicyGroup) Delete(ctx context.Context, in *sysin.ImUserPolicyGroupDeleteInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + c := dao.ImUserPolicyGroups.Columns() + mc := dao.ImUserPolicyGroupMembers.Columns() + db := imtenant.RegistryDB() + var row gdb.Record + var err error + if row, err = db.Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil { + return gerror.Wrap(err, "读取用户策略分组失败") + } + if row.IsEmpty() { + return gerror.New("用户策略分组不存在") + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, row[c.TenantId].Int64()) + if err != nil { + return err + } + count, err := db.Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).Where(mc.GroupId, in.Id).Where(mc.Status, consts.StatusEnabled).WhereNull(mc.DeletedAt).Count() + if err != nil { + return err + } + if count > 0 { + return gerror.New("请先移除分组内成员") + } + now := gtime.Now() + _, err = db.Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(g.Map{c.DeletedAt: now, c.DeletedBy: contexts.GetUserId(ctx), c.Status: consts.StatusDisable}).Update() + if err != nil { + return gerror.Wrap(err, "删除用户策略分组失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_user_policy_group_delete", "im_user_policy_group", in.Id, "success", nil) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "user_policy_group.deleted", "user_policy_groups", uint(in.Id)) +} + +func (s *sSysImUserPolicyGroup) Status(ctx context.Context, in *sysin.ImUserPolicyGroupStatusInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + c := dao.ImUserPolicyGroups.Columns() + db := imtenant.RegistryDB() + var row gdb.Record + var err error + if row, err = db.Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil { + return err + } + if row.IsEmpty() { + return gerror.New("用户策略分组不存在") + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, row[c.TenantId].Int64()) + if err != nil { + return err + } + _, err = db.Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(g.Map{c.Status: in.Status, c.Version: gdb.Raw("version+1"), c.UpdatedBy: contexts.GetUserId(ctx), c.UpdatedAt: gtime.Now()}).Update() + if err != nil { + return gerror.Wrap(err, "更新用户策略分组状态失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_user_policy_group_status", "im_user_policy_group", in.Id, "success", g.Map{"status": in.Status}) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "user_policy_group.updated", "user_policy_groups", uint(in.Id)) +} + +func (s *sSysImUserPolicyGroup) MemberList(ctx context.Context, in *sysin.ImUserPolicyGroupMemberListInp) (list []*sysin.ImUserPolicyGroupMemberModel, total int, err error) { + if err = in.Filter(ctx); err != nil { + return + } + gc := dao.ImUserPolicyGroups.Columns() + mc := dao.ImUserPolicyGroupMembers.Columns() + var group gdb.Record + if group, err = imtenant.RegistryDB().Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Where(gc.Id, in.GroupId).WhereNull(gc.DeletedAt).One(); err != nil { + return nil, 0, err + } + if group.IsEmpty() { + return nil, 0, gerror.New("用户策略分组不存在") + } + if _, _, err = resolveImPolicyTenant(ctx, group[gc.TenantId].Int64()); err != nil { + return nil, 0, err + } + model := imtenant.RegistryDB().Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).Fields(mc.Id, mc.GroupId, mc.TenantId, mc.UserId, mc.Status, mc.ValidFrom, mc.ValidUntil, mc.Remark, mc.CreatedAt).Where(mc.GroupId, in.GroupId).WhereNull(mc.DeletedAt) + if in.UserId > 0 { + model = model.Where(mc.UserId, in.UserId) + } + err = model.Page(in.Page, in.PerPage).OrderDesc(mc.Id).ScanAndCount(&list, &total, false) + return +} + +func (s *sSysImUserPolicyGroup) MemberAdd(ctx context.Context, in *sysin.ImUserPolicyGroupMemberAddInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + gc := dao.ImUserPolicyGroups.Columns() + mc := dao.ImUserPolicyGroupMembers.Columns() + db := imtenant.RegistryDB() + var group gdb.Record + var err error + if group, err = db.Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).Where(gc.Id, in.GroupId).WhereNull(gc.DeletedAt).One(); err != nil { + return err + } + if group.IsEmpty() { + return gerror.New("用户策略分组不存在") + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, group[gc.TenantId].Int64()) + if err != nil { + return err + } + if tenantID == 0 { + return gerror.New("全局默认分组不能添加成员,请先选择具体租户") + } + tenantDB, _, err := imtenant.DB(ctx, tenantID) + if err != nil { + return err + } + userCount, err := tenantDB.Model("im_users").Safe().Ctx(ctx).Where("id", in.UserId).Count() + if err != nil { + return gerror.Wrap(err, "检查租户用户失败") + } + if userCount == 0 { + return gerror.New("用户不存在或不属于当前租户") + } + var current gdb.Record + current, err = db.Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).Where(mc.GroupId, in.GroupId).Where(mc.UserId, in.UserId).One() + if err != nil { + return err + } + now := gtime.Now() + data := g.Map{mc.TenantId: tenantID, mc.Status: consts.StatusEnabled, mc.ValidFrom: in.ValidFrom, mc.ValidUntil: in.ValidUntil, mc.Remark: in.Remark, mc.DeletedAt: nil, mc.DeletedBy: nil, mc.UpdatedAt: now, mc.UpdatedBy: contexts.GetUserId(ctx)} + if current.IsEmpty() { + data[mc.GroupId] = in.GroupId + data[mc.UserId] = in.UserId + data[mc.CreatedAt] = now + data[mc.CreatedBy] = contexts.GetUserId(ctx) + _, err = db.Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).Data(data).Insert() + } else { + _, err = db.Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).Where(mc.Id, current[mc.Id]).Data(data).Update() + } + if err != nil { + return gerror.Wrap(err, "添加策略分组成员失败") + } + _ = writeImAdminAudit(ctx, tenantID, "im_user_policy_group_member_add", "im_user_policy_group", in.GroupId, "success", g.Map{"user_id": in.UserId}) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "user_policy_group.updated", "user_policy_groups", uint(in.GroupId)) +} + +func (s *sSysImUserPolicyGroup) MemberRemove(ctx context.Context, in *sysin.ImUserPolicyGroupMemberRemoveInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + mc := dao.ImUserPolicyGroupMembers.Columns() + db := imtenant.RegistryDB() + var row gdb.Record + var err error + if row, err = db.Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).Where(mc.Id, in.Id).WhereNull(mc.DeletedAt).One(); err != nil { + return err + } + if row.IsEmpty() { + return gerror.New("策略分组成员不存在") + } + tenantID, tenantCode, err := resolveImPolicyTenant(ctx, row[mc.TenantId].Int64()) + if err != nil { + return err + } + _, err = db.Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).Where(mc.Id, in.Id).Data(g.Map{mc.Status: consts.StatusDisable, mc.DeletedAt: gtime.Now(), mc.DeletedBy: contexts.GetUserId(ctx), mc.UpdatedAt: gtime.Now(), mc.UpdatedBy: contexts.GetUserId(ctx)}).Update() + if err != nil { + return gerror.Wrap(err, "移除策略分组成员失败") + } + groupID := row[mc.GroupId].Int64() + _ = writeImAdminAudit(ctx, tenantID, "im_user_policy_group_member_remove", "im_user_policy_group", groupID, "success", g.Map{"user_id": row[mc.UserId].Int64()}) + return publishImPolicyEvent(ctx, tenantID, tenantCode, "user_policy_group.updated", "user_policy_groups", uint(groupID)) +} diff --git a/server/internal/model/do/im_message_curfew_rules.go b/server/internal/model/do/im_message_curfew_rules.go new file mode 100644 index 0000000..1467e68 --- /dev/null +++ b/server/internal/model/do/im_message_curfew_rules.go @@ -0,0 +1,39 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImMessageCurfewRules is the golang structure of table im_message_curfew_rules for DAO operations like Where/Data. +type ImMessageCurfewRules struct { + g.Meta `orm:"table:im_message_curfew_rules, do:true"` + Id any // + TenantId any // + PolicyId any // + RuleCode any // + RuleName any // + DateType any // daily, weekday, weekend, custom + CustomDates *gjson.Json // YYYY-MM-DD values for custom date type + Weekdays *gjson.Json // ISO weekdays 1 through 7 + Timezone any // + StartTime *gtime.Time // + EndTime *gtime.Time // Less than start_time means crossing midnight + Scope any // private_message or group_message + WhitelistUserIds *gjson.Json // + WhitelistGroupIds *gjson.Json // + Message any // + Status any // 1 enabled, 2 disabled + Sort any // + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // + DeletedAt *gtime.Time // + DeletedBy any // +} diff --git a/server/internal/model/do/im_security_ip_rules.go b/server/internal/model/do/im_security_ip_rules.go new file mode 100644 index 0000000..99d8c0c --- /dev/null +++ b/server/internal/model/do/im_security_ip_rules.go @@ -0,0 +1,32 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImSecurityIpRules is the golang structure of table im_security_ip_rules for DAO operations like Where/Data. +type ImSecurityIpRules struct { + g.Meta `orm:"table:im_security_ip_rules, do:true"` + Id any // + TenantId any // + RuleType any // whitelist or blacklist + IpCidr any // IPv4, IPv6 or CIDR + Action any // allow, challenge, block, audit_only + SceneJson *gjson.Json // login, register, message, upload and other scenes + RiskLevel any // + Status any // 1 enabled, 2 disabled + ExpiresAt *gtime.Time // + Remark any // + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // + DeletedAt *gtime.Time // + DeletedBy any // +} diff --git a/server/internal/model/do/im_security_policy.go b/server/internal/model/do/im_security_policy.go new file mode 100644 index 0000000..e5a9ad2 --- /dev/null +++ b/server/internal/model/do/im_security_policy.go @@ -0,0 +1,39 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImSecurityPolicy is the golang structure of table im_security_policy for DAO operations like Where/Data. +type ImSecurityPolicy struct { + g.Meta `orm:"table:im_security_policy, do:true"` + Id any // + TenantId any // 0 means global fallback policy + PolicyVersion any // + PolicyName any // + PublishStatus any // + PasswordPolicy *gjson.Json // + LoginRateLimit *gjson.Json // + SensitiveWordPolicy *gjson.Json // + FileReviewPolicy *gjson.Json // + RealnamePolicy *gjson.Json // + RiskChallengePolicy *gjson.Json // + GlobalFriendEnabled any // Backend master switch for every add-friend entry + FriendDisabledPolicy *gjson.Json // Profile masking, QR and group-member restrictions + MessageCurfewEnabled any // Backend master switch for message curfew + Remark any // + PublishedAt *gtime.Time // + PublishedBy any // + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // + DeletedAt *gtime.Time // + DeletedBy any // +} diff --git a/server/internal/model/do/im_sensitive_word_groups.go b/server/internal/model/do/im_sensitive_word_groups.go new file mode 100644 index 0000000..3a4f270 --- /dev/null +++ b/server/internal/model/do/im_sensitive_word_groups.go @@ -0,0 +1,28 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImSensitiveWordGroups is the golang structure of table im_sensitive_word_groups for DAO operations like Where/Data. +type ImSensitiveWordGroups struct { + g.Meta `orm:"table:im_sensitive_word_groups, do:true"` + Id any // + TenantId any // 0 means global dictionary + GroupCode any // + GroupName any // + Priority any // + Status any // 1 enabled, 2 disabled + Remark any // + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // + DeletedAt *gtime.Time // + DeletedBy any // +} diff --git a/server/internal/model/do/im_sensitive_words.go b/server/internal/model/do/im_sensitive_words.go new file mode 100644 index 0000000..1a58efd --- /dev/null +++ b/server/internal/model/do/im_sensitive_words.go @@ -0,0 +1,35 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImSensitiveWords is the golang structure of table im_sensitive_words for DAO operations like Where/Data. +type ImSensitiveWords struct { + g.Meta `orm:"table:im_sensitive_words, do:true"` + Id any // + TenantId any // 0 means global dictionary + GroupId any // + Word any // + WordHash any // SHA-256 of normalized word + MatchType any // + SceneJson *gjson.Json // Enabled scenes + Action any // + Replacement any // + RiskLevel any // + Priority any // + Status any // 1 enabled, 2 disabled + Remark any // + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // + DeletedAt *gtime.Time // + DeletedBy any // +} diff --git a/server/internal/model/do/im_tenant_config.go b/server/internal/model/do/im_tenant_config.go index 35ac912..63edd98 100644 --- a/server/internal/model/do/im_tenant_config.go +++ b/server/internal/model/do/im_tenant_config.go @@ -11,14 +11,18 @@ import ( // ImTenantConfig is the golang structure of table im_tenant_config for DAO operations like Where/Data. type ImTenantConfig struct { - g.Meta `orm:"table:im_tenant_config, do:true"` - Id any // - TenantId any // - MaxUsers any // 0 means unlimited - MaxGroups any // 0 means unlimited - MaxGroupMembers any // 0 means unlimited - CreatedAt *gtime.Time // - CreatedBy any // - UpdatedAt *gtime.Time // - UpdatedBy any // + g.Meta `orm:"table:im_tenant_config, do:true"` + Id any // + TenantId any // + MaxUsers any // 0 means unlimited + MaxGroups any // 0 means unlimited + MaxGroupMembers any // 0 means unlimited + DefaultPolicyGroupId any // Default user policy group + SecurityPolicyVersion any // Published security policy version + GlobalFriendEnabled any // Published global friend switch + MessageCurfewEnabled any // Published message curfew switch + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // } diff --git a/server/internal/model/do/im_user_policy_group_members.go b/server/internal/model/do/im_user_policy_group_members.go new file mode 100644 index 0000000..9e00af6 --- /dev/null +++ b/server/internal/model/do/im_user_policy_group_members.go @@ -0,0 +1,29 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImUserPolicyGroupMembers is the golang structure of table im_user_policy_group_members for DAO operations like Where/Data. +type ImUserPolicyGroupMembers struct { + g.Meta `orm:"table:im_user_policy_group_members, do:true"` + Id any // + TenantId any // + GroupId any // + UserId any // User ID in the tenant database + Status any // 1 enabled, 2 disabled + ValidFrom *gtime.Time // + ValidUntil *gtime.Time // + Remark any // + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // + DeletedAt *gtime.Time // + DeletedBy any // +} diff --git a/server/internal/model/do/im_user_policy_groups.go b/server/internal/model/do/im_user_policy_groups.go new file mode 100644 index 0000000..ee4638a --- /dev/null +++ b/server/internal/model/do/im_user_policy_groups.go @@ -0,0 +1,33 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImUserPolicyGroups is the golang structure of table im_user_policy_groups for DAO operations like Where/Data. +type ImUserPolicyGroups struct { + g.Meta `orm:"table:im_user_policy_groups, do:true"` + Id any // + TenantId any // 0 means global default + GroupCode any // Stable policy group code + GroupName any // Policy group name + Priority any // Higher value has higher priority + Status any // 1 enabled, 2 disabled + PolicyJson *gjson.Json // Login, friend, message, group and file policies + IpWhitelist *gjson.Json // IPv4/IPv6 or CIDR list + IpBlacklist *gjson.Json // IPv4/IPv6 or CIDR list + Version any // Optimistic-lock/config version + Remark any // + CreatedAt *gtime.Time // + CreatedBy any // + UpdatedAt *gtime.Time // + UpdatedBy any // + DeletedAt *gtime.Time // + DeletedBy any // +} diff --git a/server/internal/model/entity/im_message_curfew_rules.go b/server/internal/model/entity/im_message_curfew_rules.go new file mode 100644 index 0000000..9dae62e --- /dev/null +++ b/server/internal/model/entity/im_message_curfew_rules.go @@ -0,0 +1,37 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImMessageCurfewRules is the golang structure for table im_message_curfew_rules. +type ImMessageCurfewRules struct { + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:""` + PolicyId int64 `json:"policyId" orm:"policy_id" description:""` + RuleCode string `json:"ruleCode" orm:"rule_code" description:""` + RuleName string `json:"ruleName" orm:"rule_name" description:""` + DateType string `json:"dateType" orm:"date_type" description:"daily, weekday, weekend, custom"` + CustomDates *gjson.Json `json:"customDates" orm:"custom_dates" description:"YYYY-MM-DD values for custom date type"` + Weekdays *gjson.Json `json:"weekdays" orm:"weekdays" description:"ISO weekdays 1 through 7"` + Timezone string `json:"timezone" orm:"timezone" description:""` + StartTime *gtime.Time `json:"startTime" orm:"start_time" description:""` + EndTime *gtime.Time `json:"endTime" orm:"end_time" description:"Less than start_time means crossing midnight"` + Scope string `json:"scope" orm:"scope" description:"private_message or group_message"` + WhitelistUserIds *gjson.Json `json:"whitelistUserIds" orm:"whitelist_user_ids" description:""` + WhitelistGroupIds *gjson.Json `json:"whitelistGroupIds" orm:"whitelist_group_ids" description:""` + Message string `json:"message" orm:"message" description:""` + Status int `json:"status" orm:"status" description:"1 enabled, 2 disabled"` + Sort int `json:"sort" orm:"sort" description:""` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` + DeletedBy int64 `json:"deletedBy" orm:"deleted_by" description:""` +} diff --git a/server/internal/model/entity/im_security_ip_rules.go b/server/internal/model/entity/im_security_ip_rules.go new file mode 100644 index 0000000..bc28762 --- /dev/null +++ b/server/internal/model/entity/im_security_ip_rules.go @@ -0,0 +1,30 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImSecurityIpRules is the golang structure for table im_security_ip_rules. +type ImSecurityIpRules struct { + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:""` + RuleType string `json:"ruleType" orm:"rule_type" description:"whitelist or blacklist"` + IpCidr string `json:"ipCidr" orm:"ip_cidr" description:"IPv4, IPv6 or CIDR"` + Action string `json:"action" orm:"action" description:"allow, challenge, block, audit_only"` + SceneJson *gjson.Json `json:"sceneJson" orm:"scene_json" description:"login, register, message, upload and other scenes"` + RiskLevel string `json:"riskLevel" orm:"risk_level" description:""` + Status int `json:"status" orm:"status" description:"1 enabled, 2 disabled"` + ExpiresAt *gtime.Time `json:"expiresAt" orm:"expires_at" description:""` + Remark string `json:"remark" orm:"remark" description:""` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` + DeletedBy int64 `json:"deletedBy" orm:"deleted_by" description:""` +} diff --git a/server/internal/model/entity/im_security_policy.go b/server/internal/model/entity/im_security_policy.go new file mode 100644 index 0000000..ccd0566 --- /dev/null +++ b/server/internal/model/entity/im_security_policy.go @@ -0,0 +1,37 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImSecurityPolicy is the golang structure for table im_security_policy. +type ImSecurityPolicy struct { + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:"0 means global fallback policy"` + PolicyVersion uint `json:"policyVersion" orm:"policy_version" description:""` + PolicyName string `json:"policyName" orm:"policy_name" description:""` + PublishStatus string `json:"publishStatus" orm:"publish_status" description:""` + PasswordPolicy *gjson.Json `json:"passwordPolicy" orm:"password_policy" description:""` + LoginRateLimit *gjson.Json `json:"loginRateLimit" orm:"login_rate_limit" description:""` + SensitiveWordPolicy *gjson.Json `json:"sensitiveWordPolicy" orm:"sensitive_word_policy" description:""` + FileReviewPolicy *gjson.Json `json:"fileReviewPolicy" orm:"file_review_policy" description:""` + RealnamePolicy *gjson.Json `json:"realnamePolicy" orm:"realname_policy" description:""` + RiskChallengePolicy *gjson.Json `json:"riskChallengePolicy" orm:"risk_challenge_policy" description:""` + GlobalFriendEnabled int `json:"globalFriendEnabled" orm:"global_friend_enabled" description:"Backend master switch for every add-friend entry"` + FriendDisabledPolicy *gjson.Json `json:"friendDisabledPolicy" orm:"friend_disabled_policy" description:"Profile masking, QR and group-member restrictions"` + MessageCurfewEnabled int `json:"messageCurfewEnabled" orm:"message_curfew_enabled" description:"Backend master switch for message curfew"` + Remark string `json:"remark" orm:"remark" description:""` + PublishedAt *gtime.Time `json:"publishedAt" orm:"published_at" description:""` + PublishedBy int64 `json:"publishedBy" orm:"published_by" description:""` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` + DeletedBy int64 `json:"deletedBy" orm:"deleted_by" description:""` +} diff --git a/server/internal/model/entity/im_sensitive_word_groups.go b/server/internal/model/entity/im_sensitive_word_groups.go new file mode 100644 index 0000000..882ba77 --- /dev/null +++ b/server/internal/model/entity/im_sensitive_word_groups.go @@ -0,0 +1,26 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// ImSensitiveWordGroups is the golang structure for table im_sensitive_word_groups. +type ImSensitiveWordGroups struct { + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:"0 means global dictionary"` + GroupCode string `json:"groupCode" orm:"group_code" description:""` + GroupName string `json:"groupName" orm:"group_name" description:""` + Priority int `json:"priority" orm:"priority" description:""` + Status int `json:"status" orm:"status" description:"1 enabled, 2 disabled"` + Remark string `json:"remark" orm:"remark" description:""` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` + DeletedBy int64 `json:"deletedBy" orm:"deleted_by" description:""` +} diff --git a/server/internal/model/entity/im_sensitive_words.go b/server/internal/model/entity/im_sensitive_words.go new file mode 100644 index 0000000..ba097ca --- /dev/null +++ b/server/internal/model/entity/im_sensitive_words.go @@ -0,0 +1,33 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImSensitiveWords is the golang structure for table im_sensitive_words. +type ImSensitiveWords struct { + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:"0 means global dictionary"` + GroupId int64 `json:"groupId" orm:"group_id" description:""` + Word string `json:"word" orm:"word" description:""` + WordHash string `json:"wordHash" orm:"word_hash" description:"SHA-256 of normalized word"` + MatchType string `json:"matchType" orm:"match_type" description:""` + SceneJson *gjson.Json `json:"sceneJson" orm:"scene_json" description:"Enabled scenes"` + Action string `json:"action" orm:"action" description:""` + Replacement string `json:"replacement" orm:"replacement" description:""` + RiskLevel string `json:"riskLevel" orm:"risk_level" description:""` + Priority int `json:"priority" orm:"priority" description:""` + Status int `json:"status" orm:"status" description:"1 enabled, 2 disabled"` + Remark string `json:"remark" orm:"remark" description:""` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` + DeletedBy int64 `json:"deletedBy" orm:"deleted_by" description:""` +} diff --git a/server/internal/model/entity/im_tenant_config.go b/server/internal/model/entity/im_tenant_config.go index a28e460..e8caeb9 100644 --- a/server/internal/model/entity/im_tenant_config.go +++ b/server/internal/model/entity/im_tenant_config.go @@ -10,13 +10,17 @@ import ( // ImTenantConfig is the golang structure for table im_tenant_config. type ImTenantConfig struct { - Id int64 `json:"id" orm:"id" description:""` - TenantId int64 `json:"tenantId" orm:"tenant_id" description:""` - MaxUsers uint64 `json:"maxUsers" orm:"max_users" description:"0 means unlimited"` - MaxGroups uint64 `json:"maxGroups" orm:"max_groups" description:"0 means unlimited"` - MaxGroupMembers uint `json:"maxGroupMembers" orm:"max_group_members" description:"0 means unlimited"` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` - CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` - UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:""` + MaxUsers uint64 `json:"maxUsers" orm:"max_users" description:"0 means unlimited"` + MaxGroups uint64 `json:"maxGroups" orm:"max_groups" description:"0 means unlimited"` + MaxGroupMembers uint `json:"maxGroupMembers" orm:"max_group_members" description:"0 means unlimited"` + DefaultPolicyGroupId int64 `json:"defaultPolicyGroupId" orm:"default_policy_group_id" description:"Default user policy group"` + SecurityPolicyVersion uint `json:"securityPolicyVersion" orm:"security_policy_version" description:"Published security policy version"` + GlobalFriendEnabled int `json:"globalFriendEnabled" orm:"global_friend_enabled" description:"Published global friend switch"` + MessageCurfewEnabled int `json:"messageCurfewEnabled" orm:"message_curfew_enabled" description:"Published message curfew switch"` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` } diff --git a/server/internal/model/entity/im_user_policy_group_members.go b/server/internal/model/entity/im_user_policy_group_members.go new file mode 100644 index 0000000..af440ba --- /dev/null +++ b/server/internal/model/entity/im_user_policy_group_members.go @@ -0,0 +1,27 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// ImUserPolicyGroupMembers is the golang structure for table im_user_policy_group_members. +type ImUserPolicyGroupMembers struct { + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:""` + GroupId int64 `json:"groupId" orm:"group_id" description:""` + UserId int64 `json:"userId" orm:"user_id" description:"User ID in the tenant database"` + Status int `json:"status" orm:"status" description:"1 enabled, 2 disabled"` + ValidFrom *gtime.Time `json:"validFrom" orm:"valid_from" description:""` + ValidUntil *gtime.Time `json:"validUntil" orm:"valid_until" description:""` + Remark string `json:"remark" orm:"remark" description:""` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` + DeletedBy int64 `json:"deletedBy" orm:"deleted_by" description:""` +} diff --git a/server/internal/model/entity/im_user_policy_groups.go b/server/internal/model/entity/im_user_policy_groups.go new file mode 100644 index 0000000..1485510 --- /dev/null +++ b/server/internal/model/entity/im_user_policy_groups.go @@ -0,0 +1,31 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gtime" +) + +// ImUserPolicyGroups is the golang structure for table im_user_policy_groups. +type ImUserPolicyGroups struct { + Id int64 `json:"id" orm:"id" description:""` + TenantId int64 `json:"tenantId" orm:"tenant_id" description:"0 means global default"` + GroupCode string `json:"groupCode" orm:"group_code" description:"Stable policy group code"` + GroupName string `json:"groupName" orm:"group_name" description:"Policy group name"` + Priority int `json:"priority" orm:"priority" description:"Higher value has higher priority"` + Status int `json:"status" orm:"status" description:"1 enabled, 2 disabled"` + PolicyJson *gjson.Json `json:"policyJson" orm:"policy_json" description:"Login, friend, message, group and file policies"` + IpWhitelist *gjson.Json `json:"ipWhitelist" orm:"ip_whitelist" description:"IPv4/IPv6 or CIDR list"` + IpBlacklist *gjson.Json `json:"ipBlacklist" orm:"ip_blacklist" description:"IPv4/IPv6 or CIDR list"` + Version uint `json:"version" orm:"version" description:"Optimistic-lock/config version"` + Remark string `json:"remark" orm:"remark" description:""` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` + CreatedBy int64 `json:"createdBy" orm:"created_by" description:""` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` + UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""` + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` + DeletedBy int64 `json:"deletedBy" orm:"deleted_by" description:""` +} diff --git a/server/internal/model/input/sysin/im_dashboard.go b/server/internal/model/input/sysin/im_dashboard.go new file mode 100644 index 0000000..9eeedee --- /dev/null +++ b/server/internal/model/input/sysin/im_dashboard.go @@ -0,0 +1,44 @@ +package sysin + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImDashboardOverviewInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` +} + +type ImDashboardOverviewModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + TotalUsers int64 `json:"totalUsers" dc:"总用户数"` + ActiveUsers int64 `json:"activeUsers" dc:"活跃用户数"` + TotalGroups int64 `json:"totalGroups" dc:"总群组数"` + TotalConversations int64 `json:"totalConversations" dc:"总会话数"` + TotalMessages int64 `json:"totalMessages" dc:"总消息数"` + TodayMessages int64 `json:"todayMessages" dc:"今日消息数"` +} + +type ImDashboardTrendsInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Days int `json:"days" dc:"统计天数"` +} + +func (in *ImDashboardTrendsInp) Filter(ctx context.Context) error { + if in.Days == 0 { + in.Days = 7 + } + if in.Days < 1 || in.Days > 31 { + return gerror.New("统计天数必须在1到31天之间") + } + return nil +} + +type ImDashboardTrendModel struct { + Date *gtime.Time `json:"date" dc:"日期"` + UserCount int64 `json:"userCount" dc:"新增用户数"` + MessageCount int64 `json:"messageCount" dc:"新增消息数"` +} diff --git a/server/internal/model/input/sysin/im_login_audit.go b/server/internal/model/input/sysin/im_login_audit.go new file mode 100644 index 0000000..c94b0b5 --- /dev/null +++ b/server/internal/model/input/sysin/im_login_audit.go @@ -0,0 +1,161 @@ +package sysin + +import ( + "context" + "hotgo/internal/model/input/form" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImUserLoginHistoryListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + UserId string `json:"userId" dc:"用户ID"` + Account string `json:"account" dc:"脱敏账号"` + LoginType string `json:"loginType" dc:"登录方式"` + EventType string `json:"eventType" dc:"事件类型"` + Platform string `json:"platform" dc:"客户端平台"` + Ip string `json:"ip" dc:"IP地址"` + LoginAt []*gtime.Time `json:"loginAt" dc:"登录时间"` +} + +func (in *ImUserLoginHistoryListInp) Filter(ctx context.Context) error { + in.Account = strings.TrimSpace(in.Account) + in.Ip = strings.TrimSpace(in.Ip) + return filterImAuditPage(&in.PageReq) +} + +type ImUserLoginHistoryModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + UserId string `json:"userId" dc:"用户ID"` + Username string `json:"username" dc:"用户名"` + AccountType string `json:"accountType" dc:"账号类型"` + AccountMask string `json:"accountMask" dc:"脱敏账号"` + LoginType string `json:"loginType" dc:"登录方式"` + EventType string `json:"eventType" dc:"事件类型"` + SessionId string `json:"sessionId" dc:"会话ID"` + DeviceId string `json:"deviceId" dc:"设备ID"` + Platform string `json:"platform" dc:"客户端平台"` + AppVersion string `json:"appVersion" dc:"客户端版本"` + Ip string `json:"ip" dc:"IP地址"` + Region string `json:"region" dc:"地区"` + UserAgent string `json:"userAgent" dc:"客户端标识"` + RiskLevel string `json:"riskLevel" dc:"风险等级"` + RequestId string `json:"requestId" dc:"请求ID"` + LoginAt *gtime.Time `json:"loginAt" dc:"登录时间"` + LogoutAt *gtime.Time `json:"logoutAt" dc:"登出时间"` + Metadata string `json:"metadata" dc:"元数据"` +} + +type ImLoginFailureLogListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + UserId string `json:"userId" dc:"用户ID"` + Account string `json:"account" dc:"脱敏账号"` + FailureType string `json:"failureType" dc:"失败类型"` + Platform string `json:"platform" dc:"客户端平台"` + Ip string `json:"ip" dc:"IP地址"` + AttemptAt []*gtime.Time `json:"attemptAt" dc:"尝试时间"` +} + +func (in *ImLoginFailureLogListInp) Filter(ctx context.Context) error { + in.Account = strings.TrimSpace(in.Account) + in.Ip = strings.TrimSpace(in.Ip) + return filterImAuditPage(&in.PageReq) +} + +func filterImAuditPage(page *form.PageReq) error { + if page.Page <= 0 { + page.Page = 1 + } + if page.PerPage <= 0 { + page.PerPage = 20 + } + if page.PerPage > 200 { + return gerror.New("每页数量不能超过200") + } + return nil +} + +type ImLoginFailureLogModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Id int64 `json:"id" dc:"ID"` + UserId string `json:"userId" dc:"用户ID"` + AccountType string `json:"accountType" dc:"账号类型"` + AccountMask string `json:"accountMask" dc:"脱敏账号"` + FailureType string `json:"failureType" dc:"失败类型"` + Reason string `json:"reason" dc:"原因码"` + Detail string `json:"detail" dc:"失败详情"` + Ip string `json:"ip" dc:"IP地址"` + Region string `json:"region" dc:"地区"` + DeviceId string `json:"deviceId" dc:"设备ID"` + Platform string `json:"platform" dc:"客户端平台"` + AppVersion string `json:"appVersion" dc:"客户端版本"` + UserAgent string `json:"userAgent" dc:"客户端标识"` + RiskLevel string `json:"riskLevel" dc:"风险等级"` + RiskAction string `json:"riskAction" dc:"风控动作"` + RequestId string `json:"requestId" dc:"请求ID"` + AttemptAt *gtime.Time `json:"attemptAt" dc:"尝试时间"` + Metadata string `json:"metadata" dc:"元数据"` +} + +type ImLoginFailureViewInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Id int64 `json:"id" dc:"日志ID"` +} + +func (in *ImLoginFailureViewInp) Filter(ctx context.Context) error { + if in.Id <= 0 { + return gerror.New("日志ID不能为空") + } + return nil +} + +type ImLoginFailureAddBlacklistInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Ip string `json:"ip" dc:"IP地址"` + Remark string `json:"remark" dc:"备注"` +} + +func (in *ImLoginFailureAddBlacklistInp) Filter(ctx context.Context) error { + in.Ip = strings.TrimSpace(in.Ip) + if in.Ip == "" { + return gerror.New("IP地址不能为空") + } + return nil +} + +type ImLoginFailureBanUserInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + UserId int64 `json:"userId" dc:"用户ID"` + Reason string `json:"reason" dc:"封禁原因"` +} + +func (in *ImLoginFailureBanUserInp) Filter(ctx context.Context) error { + in.Reason = strings.TrimSpace(in.Reason) + if in.UserId <= 0 { + return gerror.New("用户ID不能为空") + } + if in.Reason == "" { + return gerror.New("封禁原因不能为空") + } + return nil +} + +type ImLoginAuditExportModel struct { + Id int64 `json:"id" dc:"ID"` + UserId string `json:"userId" dc:"用户ID"` + AccountMask string `json:"accountMask" dc:"脱敏账号"` + EventType string `json:"eventType" dc:"事件/失败类型"` + Platform string `json:"platform" dc:"客户端平台"` + Ip string `json:"ip" dc:"IP地址"` + Region string `json:"region" dc:"地区"` + RiskLevel string `json:"riskLevel" dc:"风险等级"` + Reason string `json:"reason" dc:"原因"` + OccurredAt *gtime.Time `json:"occurredAt" dc:"发生时间"` +} diff --git a/server/internal/model/input/sysin/im_message_admin.go b/server/internal/model/input/sysin/im_message_admin.go new file mode 100644 index 0000000..e027f05 --- /dev/null +++ b/server/internal/model/input/sysin/im_message_admin.go @@ -0,0 +1,147 @@ +package sysin + +import ( + "context" + "hotgo/internal/consts" + "hotgo/internal/library/dict" + "hotgo/internal/model/input/form" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImPrivateMessageListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + Topic string `json:"topic" dc:"会话Topic"` + FromUserId string `json:"fromUserId" dc:"发送用户ID"` + ToUserId string `json:"toUserId" dc:"接收用户ID"` + ContentType string `json:"contentType" dc:"内容类型"` + Status string `json:"status" dc:"状态"` + Keyword string `json:"keyword" dc:"内容关键词"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"发送时间"` +} + +func (in *ImPrivateMessageListInp) Filter(ctx context.Context) error { + return filterImMessageList(&in.PageReq, &in.Topic, &in.Keyword, in.ContentType, in.Status) +} + +type ImGroupMessageListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID"` + Topic string `json:"topic" dc:"群Topic"` + SenderUserId string `json:"senderUserId" dc:"发送用户ID"` + ContentType string `json:"contentType" dc:"内容类型"` + Status string `json:"status" dc:"状态"` + Keyword string `json:"keyword" dc:"内容关键词"` + CreatedAt []*gtime.Time `json:"createdAt" dc:"发送时间"` +} + +func (in *ImGroupMessageListInp) Filter(ctx context.Context) error { + return filterImMessageList(&in.PageReq, &in.Topic, &in.Keyword, in.ContentType, in.Status) +} + +func filterImMessageList(page *form.PageReq, topic *string, keyword *string, contentType string, status string) error { + *topic = strings.TrimSpace(*topic) + *keyword = strings.TrimSpace(*keyword) + if page.Page <= 0 { + page.Page = 1 + } + if page.PerPage <= 0 { + page.PerPage = 20 + } + if page.PerPage > 200 { + return gerror.New("每页数量不能超过200") + } + if contentType != "" && !dict.HasOptionKey(consts.ImMessageContentTypeOptions, contentType) { + return gerror.New("消息类型不合法") + } + if status != "" && !dict.HasOptionKey(consts.ImMessageStatusOptions, status) { + return gerror.New("消息状态不合法") + } + return nil +} + +type ImMessageListModel struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + TenantName string `json:"tenantName" dc:"租户名称"` + Topic string `json:"topic" dc:"会话Topic"` + SeqId int64 `json:"seqId" dc:"消息序号"` + FromUserId string `json:"fromUserId" dc:"发送用户ID"` + SenderUsername string `json:"senderUsername" dc:"发送人用户名"` + SenderNickname string `json:"senderNickname" dc:"发送人昵称"` + ToUserId string `json:"toUserId" dc:"接收用户ID"` + ReceiverUsername string `json:"receiverUsername" dc:"接收人用户名"` + ReceiverNickname string `json:"receiverNickname" dc:"接收人昵称"` + GroupName string `json:"groupName" dc:"群名称"` + GroupNo string `json:"groupNo" dc:"群号"` + ContentType string `json:"contentType" dc:"消息类型"` + ContentExcerpt string `json:"contentExcerpt" dc:"内容摘要"` + Status string `json:"status" dc:"状态"` + ReplyToSeq int64 `json:"replyToSeq" dc:"回复消息序号"` + AtUsers string `json:"atUsers" dc:"@用户"` + CreatedAt *gtime.Time `json:"createdAt" dc:"发送时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +type ImMessageViewInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Topic string `json:"topic" dc:"会话Topic"` + SeqId int64 `json:"seqId" dc:"消息序号"` +} + +func (in *ImMessageViewInp) Filter(ctx context.Context) error { + in.Topic = strings.TrimSpace(in.Topic) + if in.Topic == "" || in.SeqId <= 0 { + return gerror.New("消息Topic和序号不能为空") + } + return nil +} + +type ImMessageViewModel struct { + ImMessageListModel + Head string `json:"head" dc:"消息头"` + Content string `json:"content" dc:"完整消息内容"` + Metadata string `json:"metadata" dc:"扩展数据"` +} + +type ImMessageActionInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + Topic string `json:"topic" dc:"会话Topic"` + SeqId int64 `json:"seqId" dc:"消息序号"` + Reason string `json:"reason" dc:"处置原因"` +} + +func (in *ImMessageActionInp) Filter(ctx context.Context) error { + in.Topic = strings.TrimSpace(in.Topic) + in.Reason = strings.TrimSpace(in.Reason) + if in.Topic == "" || in.SeqId <= 0 { + return gerror.New("消息Topic和序号不能为空") + } + if in.Reason == "" { + return gerror.New("处置原因不能为空") + } + if len([]rune(in.Reason)) > 500 { + return gerror.New("处置原因不能超过500个字符") + } + return nil +} + +type ImMessageCommandModel struct { + CommandId string `json:"commandId" dc:"命令ID"` + EventId string `json:"eventId" dc:"事件ID"` + Status string `json:"status" dc:"命令状态"` +} + +type ImMessageExportModel struct { + Topic string `json:"topic" dc:"Topic"` + SeqId int64 `json:"seqId" dc:"消息序号"` + FromUserId string `json:"fromUserId" dc:"发送用户ID"` + ToUserId string `json:"toUserId" dc:"接收用户ID"` + GroupName string `json:"groupName" dc:"群名称"` + ContentType string `json:"contentType" dc:"消息类型"` + ContentExcerpt string `json:"contentExcerpt" dc:"内容摘要"` + Status string `json:"status" dc:"状态"` + CreatedAt *gtime.Time `json:"createdAt" dc:"发送时间"` +} diff --git a/server/internal/model/input/sysin/im_policy_test.go b/server/internal/model/input/sysin/im_policy_test.go new file mode 100644 index 0000000..c1c21b4 --- /dev/null +++ b/server/internal/model/input/sysin/im_policy_test.go @@ -0,0 +1,145 @@ +package sysin + +import ( + "context" + "testing" + + "hotgo/internal/consts" + + "github.com/gogf/gf/v2/encoding/gjson" +) + +func TestImMessageCurfewRuleFilter(t *testing.T) { + ctx := context.Background() + valid := &ImMessageCurfewRuleInp{ + RuleCode: "after_work", + RuleName: "下班时段", + DateType: consts.ImCurfewDateWeekday, + StartTime: "18:00", + EndTime: "09:00", + Scope: consts.ImCurfewScopePrivate, + } + if err := valid.Filter(ctx); err != nil { + t.Fatalf("跨午夜宵禁规则应合法: %v", err) + } + if valid.Timezone != "Asia/Shanghai" || valid.Status != consts.StatusEnabled { + t.Fatalf("默认值未补齐: timezone=%s status=%d", valid.Timezone, valid.Status) + } + + invalidTime := *valid + invalidTime.StartTime = "25:00" + if err := invalidTime.Filter(ctx); err == nil { + t.Fatal("非法宵禁时间应被拒绝") + } +} + +func TestImSecurityPolicySaveFilterRequiresCurfewRule(t *testing.T) { + in := &ImSecurityPolicySaveInp{ + PolicyName: "默认策略", + GlobalFriendEnabled: 1, + MessageCurfewEnabled: 1, + } + if err := in.Filter(context.Background()); err == nil { + t.Fatal("启用宵禁但未配置规则应被拒绝") + } + in.CurfewRules = []*ImMessageCurfewRuleInp{{ + RuleCode: "group_only", + RuleName: "仅群聊", + DateType: consts.ImCurfewDateDaily, + StartTime: "18:00", + EndTime: "09:00", + Scope: consts.ImCurfewScopeGroup, + }} + if err := in.Filter(context.Background()); err == nil { + t.Fatal("宵禁仅配置群聊规则时应被拒绝") + } +} + +func TestImSecurityPolicySaveFilterForcesEveryFriendEntryOff(t *testing.T) { + in := &ImSecurityPolicySaveInp{ + PolicyName: "下班策略", + GlobalFriendEnabled: 0, + MessageCurfewEnabled: 0, + FriendDisabledPolicy: gjson.New(map[string]interface{}{ + "hide_profile": false, + "blocked_entries": []string{"search"}, + }), + } + if err := in.Filter(context.Background()); err != nil { + t.Fatalf("关闭全局好友功能的策略应合法: %v", err) + } + policy := in.FriendDisabledPolicy.Map() + if hide, ok := policy["hide_profile"].(bool); !ok || !hide { + t.Fatal("关闭全局好友功能时必须强制隐藏个人资料") + } + entries := in.FriendDisabledPolicy.Get("blocked_entries").Array() + if len(entries) != 6 { + t.Fatalf("关闭全局好友功能时必须禁用全部6个入口: %#v", policy["blocked_entries"]) + } +} + +func TestImSecurityPolicyPublishFilter(t *testing.T) { + ctx := context.Background() + if err := (&ImSecurityPolicyPublishInp{Id: 1, Reason: " "}).Filter(ctx); err == nil { + t.Fatal("空发布原因应被拒绝") + } + if err := (&ImSecurityPolicyPublishInp{Id: 1, Reason: "管理员发布"}).Filter(ctx); err != nil { + t.Fatalf("合法发布参数不应报错: %v", err) + } +} + +func TestImSecurityIPRuleEditFilter(t *testing.T) { + ctx := context.Background() + valid := &ImSecurityIPRuleEditInp{ + RuleType: "blacklist", + IpCidr: "192.0.2.0/24", + Action: consts.ImSecurityActionBlock, + SceneJson: gjson.New([]string{"login"}), + RiskLevel: consts.ImRiskLevelHigh, + } + if err := valid.Filter(ctx); err != nil { + t.Fatalf("合法IP阻断规则不应报错: %v", err) + } + + invalidAction := *valid + invalidAction.Action = consts.ImPolicyActionDeny + if err := invalidAction.Filter(ctx); err == nil { + t.Fatal("数据库枚举不支持的deny动作应被拒绝") + } +} + +func TestImSensitiveWordEditFilter(t *testing.T) { + in := &ImSensitiveWordEditInp{ + GroupId: 1, + Word: "测试词", + MatchType: consts.ImSensitiveMatchContains, + SceneJson: gjson.New([]string{"private_message"}), + Action: consts.ImSensitiveActionReplace, + RiskLevel: consts.ImRiskLevelMedium, + } + if err := in.Filter(context.Background()); err == nil { + t.Fatal("替换动作未填写替换文本应被拒绝") + } +} + +func TestImSensitiveWordGroupEditFilter(t *testing.T) { + in := &ImSensitiveWordGroupEditInp{GroupCode: "default", GroupName: " "} + if err := in.Filter(context.Background()); err == nil { + t.Fatal("空分组名称应被拒绝") + } +} + +func TestImUserPolicyGroupInputFilter(t *testing.T) { + ctx := context.Background() + group := &ImUserPolicyGroupEditInp{ + GroupCode: "staff", + GroupName: " ", + PolicyJson: gjson.New(map[string]interface{}{}), + } + if err := group.Filter(ctx); err == nil { + t.Fatal("空用户策略分组名称应被拒绝") + } + if err := (&ImUserPolicyGroupMemberAddInp{GroupId: 1, UserId: 0}).Filter(ctx); err == nil { + t.Fatal("无效用户ID应被拒绝") + } +} diff --git a/server/internal/model/input/sysin/im_security_policy.go b/server/internal/model/input/sysin/im_security_policy.go new file mode 100644 index 0000000..020a456 --- /dev/null +++ b/server/internal/model/input/sysin/im_security_policy.go @@ -0,0 +1,324 @@ +package sysin + +import ( + "context" + "strings" + "time" + + "hotgo/internal/consts" + "hotgo/internal/library/dict" + "hotgo/internal/model/input/form" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImSecurityPolicyListInp struct { + form.PageReq + TenantId int64 `json:"tenantId"` + PublishStatus string `json:"publishStatus"` +} + +func (in *ImSecurityPolicyListInp) Filter(ctx context.Context) error { + if in.PublishStatus != "" && !dict.HasOptionKey(consts.ImSecurityPublishStatusOptions, in.PublishStatus) { + return gerror.New("发布状态不合法") + } + return nil +} + +type ImMessageCurfewRuleModel struct { + Id int64 `json:"id"` + RuleCode string `json:"ruleCode"` + RuleName string `json:"ruleName"` + DateType string `json:"dateType"` + CustomDates string `json:"customDates"` + Weekdays string `json:"weekdays"` + Timezone string `json:"timezone"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + Scope string `json:"scope"` + WhitelistUserIds string `json:"whitelistUserIds"` + WhitelistGroupIds string `json:"whitelistGroupIds"` + Message string `json:"message"` + Status int `json:"status"` + Sort int `json:"sort"` +} + +type ImSecurityPolicyModel struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + PolicyVersion uint `json:"policyVersion"` + PolicyName string `json:"policyName"` + PublishStatus string `json:"publishStatus"` + PasswordPolicy string `json:"passwordPolicy"` + LoginRateLimit string `json:"loginRateLimit"` + SensitiveWordPolicy string `json:"sensitiveWordPolicy"` + FileReviewPolicy string `json:"fileReviewPolicy"` + RealnamePolicy string `json:"realnamePolicy"` + RiskChallengePolicy string `json:"riskChallengePolicy"` + GlobalFriendEnabled int `json:"globalFriendEnabled"` + FriendDisabledPolicy string `json:"friendDisabledPolicy"` + MessageCurfewEnabled int `json:"messageCurfewEnabled"` + Remark string `json:"remark"` + PublishedAt *gtime.Time `json:"publishedAt"` + PublishedBy int64 `json:"publishedBy"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` + CurfewRules []*ImMessageCurfewRuleModel `json:"curfewRules"` +} + +type ImSecurityPolicyViewInp struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` +} + +func (in *ImSecurityPolicyViewInp) Filter(ctx context.Context) error { + if in.Id <= 0 && in.TenantId < 0 { + return gerror.New("策略ID或租户ID不合法") + } + return nil +} + +type ImMessageCurfewRuleInp struct { + RuleCode string `json:"ruleCode"` + RuleName string `json:"ruleName"` + DateType string `json:"dateType"` + CustomDates *gjson.Json `json:"customDates"` + Weekdays *gjson.Json `json:"weekdays"` + Timezone string `json:"timezone"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + Scope string `json:"scope"` + WhitelistUserIds *gjson.Json `json:"whitelistUserIds"` + WhitelistGroupIds *gjson.Json `json:"whitelistGroupIds"` + Message string `json:"message"` + Status int `json:"status"` + Sort int `json:"sort"` +} + +func (in *ImMessageCurfewRuleInp) Filter(ctx context.Context) error { + in.RuleCode = strings.TrimSpace(in.RuleCode) + in.RuleName = strings.TrimSpace(in.RuleName) + in.Timezone = strings.TrimSpace(in.Timezone) + in.Message = strings.TrimSpace(in.Message) + if !imPolicyGroupCodePattern.MatchString(in.RuleCode) { + return gerror.New("宵禁规则编码格式不合法") + } + if in.RuleName == "" { + return gerror.New("宵禁规则名称不能为空") + } + if !dict.HasOptionKey(consts.ImCurfewDateTypeOptions, in.DateType) { + return gerror.New("宵禁日期类型不合法") + } + if !dict.HasOptionKey(consts.ImCurfewScopeOptions, in.Scope) { + return gerror.New("宵禁范围不合法") + } + if len(in.StartTime) != 5 || len(in.EndTime) != 5 { + return gerror.New("宵禁时间必须使用HH:mm格式") + } + if _, err := time.Parse("15:04", in.StartTime); err != nil { + return gerror.New("宵禁开始时间不合法") + } + if _, err := time.Parse("15:04", in.EndTime); err != nil { + return gerror.New("宵禁结束时间不合法") + } + if in.Timezone == "" { + in.Timezone = "Asia/Shanghai" + } + if in.Message == "" { + in.Message = "当前时段暂不能发送消息" + } + if in.Status == 0 { + in.Status = consts.StatusEnabled + } + if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable { + return gerror.New("宵禁规则状态不合法") + } + return nil +} + +type ImSecurityPolicySaveInp struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + PolicyName string `json:"policyName" v:"required#策略名称不能为空"` + PasswordPolicy *gjson.Json `json:"passwordPolicy"` + LoginRateLimit *gjson.Json `json:"loginRateLimit"` + SensitiveWordPolicy *gjson.Json `json:"sensitiveWordPolicy"` + FileReviewPolicy *gjson.Json `json:"fileReviewPolicy"` + RealnamePolicy *gjson.Json `json:"realnamePolicy"` + RiskChallengePolicy *gjson.Json `json:"riskChallengePolicy"` + GlobalFriendEnabled int `json:"globalFriendEnabled"` + FriendDisabledPolicy *gjson.Json `json:"friendDisabledPolicy"` + MessageCurfewEnabled int `json:"messageCurfewEnabled"` + Remark string `json:"remark"` + CurfewRules []*ImMessageCurfewRuleInp `json:"curfewRules"` +} + +func (in *ImSecurityPolicySaveInp) Filter(ctx context.Context) error { + in.PolicyName = strings.TrimSpace(in.PolicyName) + in.Remark = strings.TrimSpace(in.Remark) + if in.PolicyName == "" || len([]rune(in.PolicyName)) > 128 { + return gerror.New("策略名称长度必须为1至128个字符") + } + if in.GlobalFriendEnabled != 0 && in.GlobalFriendEnabled != 1 { + return gerror.New("全局好友开关不合法") + } + if in.MessageCurfewEnabled != 0 && in.MessageCurfewEnabled != 1 { + return gerror.New("宵禁开关不合法") + } + if in.GlobalFriendEnabled == 0 { + policy := make(map[string]interface{}) + if in.FriendDisabledPolicy != nil { + policy = in.FriendDisabledPolicy.Map() + } + policy["hide_profile"] = true + policy["block_qrcode"] = true + policy["block_group_member_entry"] = true + policy["blocked_entries"] = []string{ + "search", "profile_card", "group_member", "qrcode", "invite_auto_add", "recommendation", + } + message, ok := policy["message"].(string) + if !ok || strings.TrimSpace(message) == "" { + policy["message"] = "当前时段暂不允许添加好友" + } + in.FriendDisabledPolicy = gjson.New(policy) + } + if in.MessageCurfewEnabled == 1 && len(in.CurfewRules) == 0 { + return gerror.New("启用宵禁时至少配置一条宵禁规则") + } + seen := make(map[string]struct{}, len(in.CurfewRules)) + hasPrivateMessageRule := false + for _, rule := range in.CurfewRules { + if rule == nil { + return gerror.New("宵禁规则不能为空") + } + if err := rule.Filter(ctx); err != nil { + return err + } + if _, ok := seen[rule.RuleCode]; ok { + return gerror.New("宵禁规则编码不能重复") + } + seen[rule.RuleCode] = struct{}{} + if rule.Scope == consts.ImCurfewScopePrivate && rule.Status == consts.StatusEnabled { + hasPrivateMessageRule = true + } + } + if in.MessageCurfewEnabled == 1 && !hasPrivateMessageRule { + return gerror.New("启用宵禁时至少配置一条启用的单聊/私聊规则") + } + return nil +} + +type ImSecurityPolicyPublishInp struct { + Id int64 `json:"id" v:"required|min:1#策略ID不能为空|策略ID不合法"` + Reason string `json:"reason" v:"required#发布原因不能为空"` +} + +func (in *ImSecurityPolicyPublishInp) Filter(ctx context.Context) error { + in.Reason = strings.TrimSpace(in.Reason) + if in.Id <= 0 { + return gerror.New("策略ID不合法") + } + if in.Reason == "" { + return gerror.New("发布原因不能为空") + } + if len([]rune(in.Reason)) > 500 { + return gerror.New("发布原因不能超过500个字符") + } + return nil +} + +type ImSecurityPolicyRestoreInp struct { + TenantId int64 `json:"tenantId"` + Reason string `json:"reason" v:"required#恢复原因不能为空"` +} + +func (in *ImSecurityPolicyRestoreInp) Filter(ctx context.Context) error { + in.Reason = strings.TrimSpace(in.Reason) + if in.TenantId < 0 { + return gerror.New("租户ID不合法") + } + if in.Reason == "" { + return gerror.New("恢复原因不能为空") + } + if len([]rune(in.Reason)) > 500 { + return gerror.New("恢复原因不能超过500个字符") + } + return nil +} + +type ImSecurityIPRuleListInp struct { + form.PageReq + TenantId int64 `json:"tenantId"` + RuleType string `json:"ruleType"` + IpCidr string `json:"ipCidr"` + Status int `json:"status"` +} + +func (in *ImSecurityIPRuleListInp) Filter(ctx context.Context) error { + in.IpCidr = strings.TrimSpace(in.IpCidr) + if in.RuleType != "" && in.RuleType != "whitelist" && in.RuleType != "blacklist" { + return gerror.New("IP规则类型不合法") + } + return nil +} + +type ImSecurityIPRuleModel struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + RuleType string `json:"ruleType"` + IpCidr string `json:"ipCidr"` + Action string `json:"action"` + SceneJson string `json:"sceneJson"` + RiskLevel string `json:"riskLevel"` + Status int `json:"status"` + ExpiresAt *gtime.Time `json:"expiresAt"` + Remark string `json:"remark"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} +type ImSecurityIPRuleEditInp struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + RuleType string `json:"ruleType"` + IpCidr string `json:"ipCidr"` + Action string `json:"action"` + SceneJson *gjson.Json `json:"sceneJson"` + RiskLevel string `json:"riskLevel"` + Status int `json:"status"` + ExpiresAt *gtime.Time `json:"expiresAt"` + Remark string `json:"remark"` +} + +func (in *ImSecurityIPRuleEditInp) Filter(ctx context.Context) error { + in.IpCidr = strings.TrimSpace(in.IpCidr) + if in.RuleType != "whitelist" && in.RuleType != "blacklist" { + return gerror.New("IP规则类型不合法") + } + if in.IpCidr == "" { + return gerror.New("IP或CIDR不能为空") + } + if !dict.HasOptionKey(consts.ImSecurityActionOptions, in.Action) { + return gerror.New("规则动作不合法") + } + if !dict.HasOptionKey(consts.ImRiskLevelOptions, in.RiskLevel) { + return gerror.New("风险等级不合法") + } + if in.Status == 0 { + in.Status = consts.StatusEnabled + } + if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable { + return gerror.New("状态不合法") + } + if in.SceneJson == nil || in.SceneJson.String() == "" || in.SceneJson.String() == "null" { + return gerror.New("生效场景不能为空") + } + return nil +} + +type ImSecurityIPRuleDeleteInp struct { + Id int64 `json:"id" v:"required|min:1#IP规则ID不能为空|IP规则ID不合法"` +} + +func (in *ImSecurityIPRuleDeleteInp) Filter(ctx context.Context) error { return nil } diff --git a/server/internal/model/input/sysin/im_sensitive_word.go b/server/internal/model/input/sysin/im_sensitive_word.go new file mode 100644 index 0000000..f13908d --- /dev/null +++ b/server/internal/model/input/sysin/im_sensitive_word.go @@ -0,0 +1,190 @@ +package sysin + +import ( + "context" + "strings" + + "hotgo/internal/consts" + "hotgo/internal/library/dict" + "hotgo/internal/model/input/form" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +type ImSensitiveWordListInp struct { + form.PageReq + TenantId int64 `json:"tenantId"` + GroupId int64 `json:"groupId"` + Word string `json:"word"` + MatchType string `json:"matchType"` + Action string `json:"action"` + RiskLevel string `json:"riskLevel"` + Status int `json:"status"` +} + +func (in *ImSensitiveWordListInp) Filter(ctx context.Context) error { + in.Word = strings.TrimSpace(in.Word) + if in.MatchType != "" && !dict.HasOptionKey(consts.ImSensitiveMatchTypeOptions, in.MatchType) { + return gerror.New("匹配类型不合法") + } + if in.Action != "" && !dict.HasOptionKey(consts.ImSensitiveActionOptions, in.Action) { + return gerror.New("命中动作不合法") + } + if in.RiskLevel != "" && !dict.HasOptionKey(consts.ImRiskLevelOptions, in.RiskLevel) { + return gerror.New("风险等级不合法") + } + return nil +} + +type ImSensitiveWordListModel struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + GroupId int64 `json:"groupId"` + GroupName string `json:"groupName"` + Word string `json:"word"` + MatchType string `json:"matchType"` + SceneJson string `json:"sceneJson"` + Action string `json:"action"` + Replacement string `json:"replacement"` + RiskLevel string `json:"riskLevel"` + Priority int `json:"priority"` + Status int `json:"status"` + Remark string `json:"remark"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +type ImSensitiveWordEditInp struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + GroupId int64 `json:"groupId" v:"required|min:1#敏感词分组不能为空|敏感词分组不合法"` + Word string `json:"word" v:"required#敏感词不能为空"` + MatchType string `json:"matchType"` + SceneJson *gjson.Json `json:"sceneJson" v:"required#生效场景不能为空"` + Action string `json:"action"` + Replacement string `json:"replacement"` + RiskLevel string `json:"riskLevel"` + Priority int `json:"priority"` + Status int `json:"status"` + Remark string `json:"remark"` +} + +func (in *ImSensitiveWordEditInp) Filter(ctx context.Context) error { + in.Word = strings.TrimSpace(in.Word) + in.Replacement = strings.TrimSpace(in.Replacement) + in.Remark = strings.TrimSpace(in.Remark) + if in.Word == "" || len([]rune(in.Word)) > 255 { + return gerror.New("敏感词长度必须为1至255个字符") + } + if !dict.HasOptionKey(consts.ImSensitiveMatchTypeOptions, in.MatchType) { + return gerror.New("匹配类型不合法") + } + if !dict.HasOptionKey(consts.ImSensitiveActionOptions, in.Action) { + return gerror.New("命中动作不合法") + } + if !dict.HasOptionKey(consts.ImRiskLevelOptions, in.RiskLevel) { + return gerror.New("风险等级不合法") + } + if in.Status == 0 { + in.Status = consts.StatusEnabled + } + if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable { + return gerror.New("状态不合法") + } + if in.SceneJson == nil || in.SceneJson.String() == "" || in.SceneJson.String() == "null" { + return gerror.New("生效场景不能为空") + } + if in.Action == consts.ImSensitiveActionReplace && in.Replacement == "" { + return gerror.New("替换动作必须填写替换文本") + } + return nil +} + +type ImSensitiveWordDeleteInp struct { + Id int64 `json:"id" v:"required|min:1#敏感词ID不能为空|敏感词ID不合法"` +} + +func (in *ImSensitiveWordDeleteInp) Filter(ctx context.Context) error { return nil } + +type ImSensitiveWordStatusInp struct { + Id int64 `json:"id" v:"required|min:1#敏感词ID不能为空|敏感词ID不合法"` + Status int `json:"status" v:"required#状态不能为空"` +} + +func (in *ImSensitiveWordStatusInp) Filter(ctx context.Context) error { + if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable { + return gerror.New("状态不合法") + } + return nil +} + +type ImSensitiveWordBatchImportInp struct { + TenantId int64 `json:"tenantId"` + GroupId int64 `json:"groupId" v:"required|min:1#敏感词分组不能为空|敏感词分组不合法"` + Words []string `json:"words" v:"required#敏感词列表不能为空"` + MatchType string `json:"matchType"` + SceneJson *gjson.Json `json:"sceneJson"` + Action string `json:"action"` + Replacement string `json:"replacement"` + RiskLevel string `json:"riskLevel"` +} + +func (in *ImSensitiveWordBatchImportInp) Filter(ctx context.Context) error { + if len(in.Words) == 0 || len(in.Words) > 5000 { + return gerror.New("每次导入1至5000个敏感词") + } + base := ImSensitiveWordEditInp{GroupId: in.GroupId, Word: "validate", MatchType: in.MatchType, SceneJson: in.SceneJson, Action: in.Action, Replacement: in.Replacement, RiskLevel: in.RiskLevel, Status: consts.StatusEnabled} + return base.Filter(ctx) +} + +type ImSensitiveWordBatchImportModel struct { + Inserted int `json:"inserted"` + Skipped int `json:"skipped"` +} + +type ImSensitiveWordGroupListInp struct { + TenantId int64 `json:"tenantId"` +} + +func (in *ImSensitiveWordGroupListInp) Filter(ctx context.Context) error { return nil } + +type ImSensitiveWordGroupModel struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + GroupCode string `json:"groupCode"` + GroupName string `json:"groupName"` + Priority int `json:"priority"` + Status int `json:"status"` + Remark string `json:"remark"` + CreatedAt *gtime.Time `json:"createdAt"` +} +type ImSensitiveWordGroupEditInp struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + GroupCode string `json:"groupCode" v:"required#分组编码不能为空"` + GroupName string `json:"groupName" v:"required#分组名称不能为空"` + Priority int `json:"priority"` + Status int `json:"status"` + Remark string `json:"remark"` +} + +func (in *ImSensitiveWordGroupEditInp) Filter(ctx context.Context) error { + in.GroupCode = strings.TrimSpace(in.GroupCode) + in.GroupName = strings.TrimSpace(in.GroupName) + in.Remark = strings.TrimSpace(in.Remark) + if !imPolicyGroupCodePattern.MatchString(in.GroupCode) { + return gerror.New("分组编码格式不合法") + } + if in.GroupName == "" || len([]rune(in.GroupName)) > 128 { + return gerror.New("分组名称长度必须为1至128个字符") + } + if in.Status == 0 { + in.Status = consts.StatusEnabled + } + if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable { + return gerror.New("状态不合法") + } + return nil +} diff --git a/server/internal/model/input/sysin/im_user_policy_group.go b/server/internal/model/input/sysin/im_user_policy_group.go new file mode 100644 index 0000000..6fb35cd --- /dev/null +++ b/server/internal/model/input/sysin/im_user_policy_group.go @@ -0,0 +1,157 @@ +package sysin + +import ( + "context" + "regexp" + "strings" + + "hotgo/internal/consts" + "hotgo/internal/model/input/form" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" +) + +var imPolicyGroupCodePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{1,63}$`) + +type ImUserPolicyGroupListInp struct { + form.PageReq + TenantId int64 `json:"tenantId" dc:"租户ID,0为全局默认"` + Keyword string `json:"keyword" dc:"编码或名称"` + Status int `json:"status" dc:"状态"` +} + +func (in *ImUserPolicyGroupListInp) Filter(ctx context.Context) error { + in.Keyword = strings.TrimSpace(in.Keyword) + if in.Status != 0 && in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable { + return gerror.New("状态不合法") + } + return nil +} + +type ImUserPolicyGroupListModel struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId"` + GroupCode string `json:"groupCode"` + GroupName string `json:"groupName"` + Priority int `json:"priority"` + Status int `json:"status"` + PolicyJson string `json:"policyJson"` + IpWhitelist string `json:"ipWhitelist"` + IpBlacklist string `json:"ipBlacklist"` + Version uint `json:"version"` + Remark string `json:"remark"` + MemberCount int `json:"memberCount"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +type ImUserPolicyGroupViewInp struct { + Id int64 `json:"id" v:"required|min:1#策略分组ID不能为空|策略分组ID不合法"` +} + +func (in *ImUserPolicyGroupViewInp) Filter(ctx context.Context) error { return nil } + +type ImUserPolicyGroupViewModel struct{ ImUserPolicyGroupListModel } + +type ImUserPolicyGroupEditInp struct { + Id int64 `json:"id"` + TenantId int64 `json:"tenantId" dc:"租户ID,0为全局默认"` + GroupCode string `json:"groupCode" v:"required#分组编码不能为空"` + GroupName string `json:"groupName" v:"required#分组名称不能为空"` + Priority int `json:"priority"` + Status int `json:"status"` + PolicyJson *gjson.Json `json:"policyJson" v:"required#策略配置不能为空"` + IpWhitelist *gjson.Json `json:"ipWhitelist"` + IpBlacklist *gjson.Json `json:"ipBlacklist"` + Remark string `json:"remark"` +} + +func (in *ImUserPolicyGroupEditInp) Filter(ctx context.Context) error { + in.GroupCode = strings.TrimSpace(in.GroupCode) + in.GroupName = strings.TrimSpace(in.GroupName) + in.Remark = strings.TrimSpace(in.Remark) + if !imPolicyGroupCodePattern.MatchString(in.GroupCode) { + return gerror.New("分组编码必须以字母开头,只能包含字母、数字、下划线和中划线") + } + if in.GroupName == "" || len([]rune(in.GroupName)) > 128 { + return gerror.New("分组名称长度必须为1至128个字符") + } + if in.Status == 0 { + in.Status = consts.StatusEnabled + } + if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable { + return gerror.New("状态不合法") + } + if in.PolicyJson == nil || in.PolicyJson.String() == "null" { + return gerror.New("策略配置不能为空") + } + return nil +} + +type ImUserPolicyGroupDeleteInp struct { + Id int64 `json:"id" v:"required|min:1#策略分组ID不能为空|策略分组ID不合法"` +} + +func (in *ImUserPolicyGroupDeleteInp) Filter(ctx context.Context) error { return nil } + +type ImUserPolicyGroupStatusInp struct { + Id int64 `json:"id" v:"required|min:1#策略分组ID不能为空|策略分组ID不合法"` + Status int `json:"status" v:"required#状态不能为空"` +} + +func (in *ImUserPolicyGroupStatusInp) Filter(ctx context.Context) error { + if in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable { + return gerror.New("状态不合法") + } + return nil +} + +type ImUserPolicyGroupMemberListInp struct { + form.PageReq + GroupId int64 `json:"groupId" v:"required|min:1#策略分组ID不能为空|策略分组ID不合法"` + UserId int64 `json:"userId"` +} + +func (in *ImUserPolicyGroupMemberListInp) Filter(ctx context.Context) error { return nil } + +type ImUserPolicyGroupMemberModel struct { + Id int64 `json:"id"` + GroupId int64 `json:"groupId"` + TenantId int64 `json:"tenantId"` + UserId int64 `json:"userId"` + Status int `json:"status"` + ValidFrom *gtime.Time `json:"validFrom"` + ValidUntil *gtime.Time `json:"validUntil"` + Remark string `json:"remark"` + CreatedAt *gtime.Time `json:"createdAt"` +} + +type ImUserPolicyGroupMemberAddInp struct { + GroupId int64 `json:"groupId" v:"required|min:1#策略分组ID不能为空|策略分组ID不合法"` + UserId int64 `json:"userId" v:"required|min:1#用户ID不能为空|用户ID不合法"` + ValidFrom *gtime.Time `json:"validFrom"` + ValidUntil *gtime.Time `json:"validUntil"` + Remark string `json:"remark"` +} + +func (in *ImUserPolicyGroupMemberAddInp) Filter(ctx context.Context) error { + in.Remark = strings.TrimSpace(in.Remark) + if in.GroupId <= 0 { + return gerror.New("策略分组ID不合法") + } + if in.UserId <= 0 { + return gerror.New("用户ID不合法") + } + if in.ValidFrom != nil && in.ValidUntil != nil && in.ValidUntil.Before(in.ValidFrom) { + return gerror.New("有效结束时间不能早于开始时间") + } + return nil +} + +type ImUserPolicyGroupMemberRemoveInp struct { + Id int64 `json:"id" v:"required|min:1#成员关系ID不能为空|成员关系ID不合法"` +} + +func (in *ImUserPolicyGroupMemberRemoveInp) Filter(ctx context.Context) error { return nil } diff --git a/server/internal/router/genrouter/im_dashboard.go b/server/internal/router/genrouter/im_dashboard.go new file mode 100644 index 0000000..d5aa19a --- /dev/null +++ b/server/internal/router/genrouter/im_dashboard.go @@ -0,0 +1,5 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.ImDashboard) } diff --git a/server/internal/router/genrouter/im_group_message.go b/server/internal/router/genrouter/im_group_message.go new file mode 100644 index 0000000..956c32a --- /dev/null +++ b/server/internal/router/genrouter/im_group_message.go @@ -0,0 +1,5 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.ImGroupMessage) } diff --git a/server/internal/router/genrouter/im_login_failure_log.go b/server/internal/router/genrouter/im_login_failure_log.go new file mode 100644 index 0000000..69c8d31 --- /dev/null +++ b/server/internal/router/genrouter/im_login_failure_log.go @@ -0,0 +1,5 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.ImLoginFailureLog) } diff --git a/server/internal/router/genrouter/im_private_message.go b/server/internal/router/genrouter/im_private_message.go new file mode 100644 index 0000000..c3827ff --- /dev/null +++ b/server/internal/router/genrouter/im_private_message.go @@ -0,0 +1,5 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.ImPrivateMessage) } diff --git a/server/internal/router/genrouter/im_security_policy.go b/server/internal/router/genrouter/im_security_policy.go new file mode 100644 index 0000000..8dcb15d --- /dev/null +++ b/server/internal/router/genrouter/im_security_policy.go @@ -0,0 +1,5 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.ImSecurityPolicy) } diff --git a/server/internal/router/genrouter/im_sensitive_word.go b/server/internal/router/genrouter/im_sensitive_word.go new file mode 100644 index 0000000..d40be89 --- /dev/null +++ b/server/internal/router/genrouter/im_sensitive_word.go @@ -0,0 +1,5 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.ImSensitiveWord) } diff --git a/server/internal/router/genrouter/im_user_login_history.go b/server/internal/router/genrouter/im_user_login_history.go new file mode 100644 index 0000000..6940376 --- /dev/null +++ b/server/internal/router/genrouter/im_user_login_history.go @@ -0,0 +1,5 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.ImUserLoginHistory) } diff --git a/server/internal/router/genrouter/im_user_policy_group.go b/server/internal/router/genrouter/im_user_policy_group.go new file mode 100644 index 0000000..1ba01b7 --- /dev/null +++ b/server/internal/router/genrouter/im_user_policy_group.go @@ -0,0 +1,5 @@ +package genrouter + +import "hotgo/internal/controller/admin/sys" + +func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.ImUserPolicyGroup) } diff --git a/server/internal/service/sys.go b/server/internal/service/sys.go index b79497d..6f55bbc 100644 --- a/server/internal/service/sys.go +++ b/server/internal/service/sys.go @@ -359,6 +359,64 @@ type ( // Update 更新IM文件策略 Update(ctx context.Context, in *sysin.ImFilePolicyUpdateInp) (err error) } + ISysImUserPolicyGroup interface { + List(ctx context.Context, in *sysin.ImUserPolicyGroupListInp) (list []*sysin.ImUserPolicyGroupListModel, totalCount int, err error) + View(ctx context.Context, in *sysin.ImUserPolicyGroupViewInp) (res *sysin.ImUserPolicyGroupViewModel, err error) + Edit(ctx context.Context, in *sysin.ImUserPolicyGroupEditInp) (err error) + Delete(ctx context.Context, in *sysin.ImUserPolicyGroupDeleteInp) (err error) + Status(ctx context.Context, in *sysin.ImUserPolicyGroupStatusInp) (err error) + MemberList(ctx context.Context, in *sysin.ImUserPolicyGroupMemberListInp) (list []*sysin.ImUserPolicyGroupMemberModel, totalCount int, err error) + MemberAdd(ctx context.Context, in *sysin.ImUserPolicyGroupMemberAddInp) (err error) + MemberRemove(ctx context.Context, in *sysin.ImUserPolicyGroupMemberRemoveInp) (err error) + } + ISysImSensitiveWord interface { + List(ctx context.Context, in *sysin.ImSensitiveWordListInp) (list []*sysin.ImSensitiveWordListModel, totalCount int, err error) + Edit(ctx context.Context, in *sysin.ImSensitiveWordEditInp) (err error) + Delete(ctx context.Context, in *sysin.ImSensitiveWordDeleteInp) (err error) + Status(ctx context.Context, in *sysin.ImSensitiveWordStatusInp) (err error) + BatchImport(ctx context.Context, in *sysin.ImSensitiveWordBatchImportInp) (res *sysin.ImSensitiveWordBatchImportModel, err error) + GroupList(ctx context.Context, in *sysin.ImSensitiveWordGroupListInp) (list []*sysin.ImSensitiveWordGroupModel, err error) + GroupEdit(ctx context.Context, in *sysin.ImSensitiveWordGroupEditInp) (err error) + } + ISysImSecurityPolicy interface { + List(ctx context.Context, in *sysin.ImSecurityPolicyListInp) (list []*sysin.ImSecurityPolicyModel, totalCount int, err error) + View(ctx context.Context, in *sysin.ImSecurityPolicyViewInp) (res *sysin.ImSecurityPolicyModel, err error) + Save(ctx context.Context, in *sysin.ImSecurityPolicySaveInp) (id int64, err error) + Publish(ctx context.Context, in *sysin.ImSecurityPolicyPublishInp) (err error) + RestoreDefault(ctx context.Context, in *sysin.ImSecurityPolicyRestoreInp) (id int64, err error) + IPRuleList(ctx context.Context, in *sysin.ImSecurityIPRuleListInp) (list []*sysin.ImSecurityIPRuleModel, totalCount int, err error) + IPRuleEdit(ctx context.Context, in *sysin.ImSecurityIPRuleEditInp) (err error) + IPRuleDelete(ctx context.Context, in *sysin.ImSecurityIPRuleDeleteInp) (err error) + } + ISysImDashboard interface { + Overview(ctx context.Context, in *sysin.ImDashboardOverviewInp) (res *sysin.ImDashboardOverviewModel, err error) + Trends(ctx context.Context, in *sysin.ImDashboardTrendsInp) (list []*sysin.ImDashboardTrendModel, err error) + } + ISysImPrivateMessage interface { + List(ctx context.Context, in *sysin.ImPrivateMessageListInp) (list []*sysin.ImMessageListModel, totalCount int, err error) + View(ctx context.Context, in *sysin.ImMessageViewInp) (res *sysin.ImMessageViewModel, err error) + Hide(ctx context.Context, in *sysin.ImMessageActionInp) (res *sysin.ImMessageCommandModel, err error) + Delete(ctx context.Context, in *sysin.ImMessageActionInp) (res *sysin.ImMessageCommandModel, err error) + Export(ctx context.Context, in *sysin.ImPrivateMessageListInp) (err error) + } + ISysImGroupMessage interface { + List(ctx context.Context, in *sysin.ImGroupMessageListInp) (list []*sysin.ImMessageListModel, totalCount int, err error) + View(ctx context.Context, in *sysin.ImMessageViewInp) (res *sysin.ImMessageViewModel, err error) + Hide(ctx context.Context, in *sysin.ImMessageActionInp) (res *sysin.ImMessageCommandModel, err error) + Delete(ctx context.Context, in *sysin.ImMessageActionInp) (res *sysin.ImMessageCommandModel, err error) + Export(ctx context.Context, in *sysin.ImGroupMessageListInp) (err error) + } + ISysImUserLoginHistory interface { + List(ctx context.Context, in *sysin.ImUserLoginHistoryListInp) (list []*sysin.ImUserLoginHistoryModel, totalCount int, err error) + Export(ctx context.Context, in *sysin.ImUserLoginHistoryListInp) (err error) + } + ISysImLoginFailureLog interface { + List(ctx context.Context, in *sysin.ImLoginFailureLogListInp) (list []*sysin.ImLoginFailureLogModel, totalCount int, err error) + View(ctx context.Context, in *sysin.ImLoginFailureViewInp) (res *sysin.ImLoginFailureLogModel, err error) + Export(ctx context.Context, in *sysin.ImLoginFailureLogListInp) (err error) + AddBlacklist(ctx context.Context, in *sysin.ImLoginFailureAddBlacklistInp) (err error) + BanUser(ctx context.Context, in *sysin.ImLoginFailureBanUserInp) (res *sysin.ImUserCommandModel, err error) + } ISysImExternalConfig interface { // List 获取IM外部服务配置列表 List(ctx context.Context, in *sysin.ImExternalConfigListInp) (list []*sysin.ImExternalConfigListModel, totalCount int, err error) @@ -686,50 +744,58 @@ type ( ) var ( - localSysAddons ISysAddons - localSysAddonsConfig ISysAddonsConfig - localSysAttachment ISysAttachment - localSysBlacklist ISysBlacklist - localSysConfig ISysConfig - localSysCron ISysCron - localSysCronGroup ISysCronGroup - localSysCurdDemo ISysCurdDemo - localSysImTenant ISysImTenant - localSysImRegistration ISysImRegistration - localSysImInvite ISysImInvite - localSysImUser ISysImUser - localSysImGroup ISysImGroup - localSysImRealname ISysImRealname - localSysImWorkOrder ISysImWorkOrder - localSysImOutbox ISysImOutbox - localSysImOrg ISysImOrg - localSysImFile ISysImFile - localSysImFilePolicy ISysImFilePolicy - localSysImExternalConfig ISysImExternalConfig - localSysImNotification ISysImNotification - localSysImAppRelease ISysImAppRelease - localSysImAbuseReason ISysImAbuseReason - localSysImMailboxConfig ISysImMailboxConfig - localSysImFeedbackConfig ISysImFeedbackConfig - localSysImWorkbench ISysImWorkbench - localSysImTaskDashboard ISysImTaskDashboard - localSysImAuditLog ISysImAuditLog - localSysImKafkaAudit ISysImKafkaAudit - localSysImKafkaCommand ISysImKafkaCommand - localSysImDeadLetter ISysImDeadLetter - localSysDictData ISysDictData - localSysDictType ISysDictType - localSysEmsLog ISysEmsLog - localSysGenCodes ISysGenCodes - localSysLog ISysLog - localSysLoginLog ISysLoginLog - localSysNormalTreeDemo ISysNormalTreeDemo - localSysOptionTreeDemo ISysOptionTreeDemo - localSysProvinces ISysProvinces - localSysServeLicense ISysServeLicense - localSysServeLog ISysServeLog - localSysSmsLog ISysSmsLog - localSysTestCategory ISysTestCategory + localSysAddons ISysAddons + localSysAddonsConfig ISysAddonsConfig + localSysAttachment ISysAttachment + localSysBlacklist ISysBlacklist + localSysConfig ISysConfig + localSysCron ISysCron + localSysCronGroup ISysCronGroup + localSysCurdDemo ISysCurdDemo + localSysImTenant ISysImTenant + localSysImRegistration ISysImRegistration + localSysImInvite ISysImInvite + localSysImUser ISysImUser + localSysImGroup ISysImGroup + localSysImRealname ISysImRealname + localSysImWorkOrder ISysImWorkOrder + localSysImOutbox ISysImOutbox + localSysImOrg ISysImOrg + localSysImFile ISysImFile + localSysImFilePolicy ISysImFilePolicy + localSysImUserPolicyGroup ISysImUserPolicyGroup + localSysImSensitiveWord ISysImSensitiveWord + localSysImSecurityPolicy ISysImSecurityPolicy + localSysImDashboard ISysImDashboard + localSysImPrivateMessage ISysImPrivateMessage + localSysImGroupMessage ISysImGroupMessage + localSysImUserLoginHistory ISysImUserLoginHistory + localSysImLoginFailureLog ISysImLoginFailureLog + localSysImExternalConfig ISysImExternalConfig + localSysImNotification ISysImNotification + localSysImAppRelease ISysImAppRelease + localSysImAbuseReason ISysImAbuseReason + localSysImMailboxConfig ISysImMailboxConfig + localSysImFeedbackConfig ISysImFeedbackConfig + localSysImWorkbench ISysImWorkbench + localSysImTaskDashboard ISysImTaskDashboard + localSysImAuditLog ISysImAuditLog + localSysImKafkaAudit ISysImKafkaAudit + localSysImKafkaCommand ISysImKafkaCommand + localSysImDeadLetter ISysImDeadLetter + localSysDictData ISysDictData + localSysDictType ISysDictType + localSysEmsLog ISysEmsLog + localSysGenCodes ISysGenCodes + localSysLog ISysLog + localSysLoginLog ISysLoginLog + localSysNormalTreeDemo ISysNormalTreeDemo + localSysOptionTreeDemo ISysOptionTreeDemo + localSysProvinces ISysProvinces + localSysServeLicense ISysServeLicense + localSysServeLog ISysServeLog + localSysSmsLog ISysSmsLog + localSysTestCategory ISysTestCategory ) func SysAddons() ISysAddons { @@ -941,6 +1007,78 @@ func RegisterSysImFilePolicy(i ISysImFilePolicy) { localSysImFilePolicy = i } +func SysImUserPolicyGroup() ISysImUserPolicyGroup { + if localSysImUserPolicyGroup == nil { + panic("implement not found for interface ISysImUserPolicyGroup, forgot register?") + } + return localSysImUserPolicyGroup +} + +func RegisterSysImUserPolicyGroup(i ISysImUserPolicyGroup) { localSysImUserPolicyGroup = i } + +func SysImSensitiveWord() ISysImSensitiveWord { + if localSysImSensitiveWord == nil { + panic("implement not found for interface ISysImSensitiveWord, forgot register?") + } + return localSysImSensitiveWord +} + +func RegisterSysImSensitiveWord(i ISysImSensitiveWord) { localSysImSensitiveWord = i } + +func SysImSecurityPolicy() ISysImSecurityPolicy { + if localSysImSecurityPolicy == nil { + panic("implement not found for interface ISysImSecurityPolicy, forgot register?") + } + return localSysImSecurityPolicy +} + +func RegisterSysImSecurityPolicy(i ISysImSecurityPolicy) { localSysImSecurityPolicy = i } + +func SysImDashboard() ISysImDashboard { + if localSysImDashboard == nil { + panic("implement not found for interface ISysImDashboard, forgot register?") + } + return localSysImDashboard +} + +func RegisterSysImDashboard(i ISysImDashboard) { localSysImDashboard = i } + +func SysImPrivateMessage() ISysImPrivateMessage { + if localSysImPrivateMessage == nil { + panic("implement not found for interface ISysImPrivateMessage, forgot register?") + } + return localSysImPrivateMessage +} + +func RegisterSysImPrivateMessage(i ISysImPrivateMessage) { localSysImPrivateMessage = i } + +func SysImGroupMessage() ISysImGroupMessage { + if localSysImGroupMessage == nil { + panic("implement not found for interface ISysImGroupMessage, forgot register?") + } + return localSysImGroupMessage +} + +func RegisterSysImGroupMessage(i ISysImGroupMessage) { localSysImGroupMessage = i } + +func SysImUserLoginHistory() ISysImUserLoginHistory { + if localSysImUserLoginHistory == nil { + panic("implement not found for interface ISysImUserLoginHistory, forgot register?") + } + return localSysImUserLoginHistory +} + +func RegisterSysImUserLoginHistory(i ISysImUserLoginHistory) { localSysImUserLoginHistory = i } + +func SysImLoginFailureLog() ISysImLoginFailureLog { + if localSysImLoginFailureLog == nil { + panic("implement not found for interface ISysImLoginFailureLog, forgot register?") + } + return localSysImLoginFailureLog +} + +func RegisterSysImLoginFailureLog(i ISysImLoginFailureLog) { localSysImLoginFailureLog = i } + func SysImExternalConfig() ISysImExternalConfig { if localSysImExternalConfig == nil { panic("implement not found for interface ISysImExternalConfig, forgot register?") diff --git a/server/storage/data/generate/im_adfahi_batch2_menu.sql b/server/storage/data/generate/im_adfahi_batch2_menu.sql new file mode 100644 index 0000000..eba9167 --- /dev/null +++ b/server/storage/data/generate/im_adfahi_batch2_menu.sql @@ -0,0 +1,38 @@ +SET @now := NOW(); +SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT NULL,@imRootId,p.title,p.name,p.path,'','2','',p.permissions,'',p.component,'1','imManager','0','0','','0','0','0','2',CONCAT('tr_',@imRootId,' '),p.sort,'','1',@now,@now +FROM ( + SELECT '数据概览' title,'imDashboardIndex' name,'dashboard/index' path,'/imDashboard/overview' permissions,'/imDashboard/index' component,5 sort UNION ALL + SELECT '单聊消息','imPrivateMessageIndex','private-message/index','/imPrivateMessage/list','/imPrivateMessage/index',40 UNION ALL + SELECT '群聊消息','imGroupMessageIndex','group-message/index','/imGroupMessage/list','/imGroupMessage/index',45 UNION ALL + SELECT '用户登录历史','imUserLoginHistoryIndex','user-login-history/index','/imUserLoginHistory/list','/imUserLoginHistory/index',50 UNION ALL + SELECT '登录失败日志','imLoginFailureLogIndex','login-failure-log/index','/imLoginFailureLog/list','/imLoginFailureLog/index',55 +) p WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` m WHERE m.`name`=p.name); + +SET @dashboardId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='imDashboardIndex' LIMIT 1); +SET @privateMessageId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='imPrivateMessageIndex' LIMIT 1); +SET @groupMessageId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='imGroupMessageIndex' LIMIT 1); +SET @userLoginId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='imUserLoginHistoryIndex' LIMIT 1); +SET @loginFailureId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='imLoginFailureLogIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT NULL,x.pid,x.title,x.name,'','','3','',x.permissions,'','','1','','0','0','','0','0','0','3',CONCAT('tr_',@imRootId,' tr_',x.pid,' '),x.sort,'','1',@now,@now +FROM ( + SELECT @dashboardId pid,'查看概览' title,'imDashboardOverview' name,'/imDashboard/overview' permissions,10 sort UNION ALL + SELECT @dashboardId,'查看趋势','imDashboardTrends','/imDashboard/trends',20 UNION ALL + SELECT @privateMessageId,'消息详情','imPrivateMessageView','/imPrivateMessage/view',10 UNION ALL + SELECT @privateMessageId,'隐藏消息','imPrivateMessageHide','/imPrivateMessage/hide',20 UNION ALL + SELECT @privateMessageId,'删除消息','imPrivateMessageDelete','/imPrivateMessage/delete',30 UNION ALL + SELECT @privateMessageId,'导出消息','imPrivateMessageExport','/imPrivateMessage/export',40 UNION ALL + SELECT @groupMessageId,'消息详情','imGroupMessageView','/imGroupMessage/view',10 UNION ALL + SELECT @groupMessageId,'隐藏消息','imGroupMessageHide','/imGroupMessage/hide',20 UNION ALL + SELECT @groupMessageId,'删除消息','imGroupMessageDelete','/imGroupMessage/delete',30 UNION ALL + SELECT @groupMessageId,'导出消息','imGroupMessageExport','/imGroupMessage/export',40 UNION ALL + SELECT @userLoginId,'导出登录历史','imUserLoginHistoryExport','/imUserLoginHistory/export',10 UNION ALL + SELECT @loginFailureId,'失败详情','imLoginFailureLogView','/imLoginFailureLog/view',10 UNION ALL + SELECT @loginFailureId,'导出失败日志','imLoginFailureLogExport','/imLoginFailureLog/export',20 UNION ALL + SELECT @loginFailureId,'加入IP黑名单','imLoginFailureLogAddBlacklist','/imLoginFailureLog/addBlacklist',30 UNION ALL + SELECT @loginFailureId,'封禁关联用户','imLoginFailureLogBanUser','/imLoginFailureLog/banUser',40 +) x WHERE x.pid IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` m WHERE m.`name`=x.name); diff --git a/server/storage/data/generate/im_adfahi_policy_menu.sql b/server/storage/data/generate/im_adfahi_policy_menu.sql new file mode 100644 index 0000000..5c6dbde --- /dev/null +++ b/server/storage/data/generate/im_adfahi_policy_menu.sql @@ -0,0 +1,52 @@ +SET @now := NOW(); +SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT NULL,@imRootId,'用户分组','imUserPolicyGroupIndex','user-policy-group/index','','2','','/imUserPolicyGroup/list','','/imUserPolicyGroup/index','1','imManager','0','0','','0','0','0','2',CONCAT('tr_',@imRootId,' '),'25','','1',@now,@now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name`='imUserPolicyGroupIndex'); +SET @policyGroupId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='imUserPolicyGroupIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT NULL,@policyGroupId,p.title,p.name,'','','3','',p.permissions,'','','1','','0','0','','0','0','0','3',CONCAT('tr_',@imRootId,' tr_',@policyGroupId,' '),p.sort,'','1',@now,@now +FROM ( + SELECT '分组详情' title,'imUserPolicyGroupView' name,'/imUserPolicyGroup/view' permissions,10 sort UNION ALL + SELECT '分组编辑','imUserPolicyGroupEdit','/imUserPolicyGroup/edit',20 UNION ALL + SELECT '分组删除','imUserPolicyGroupDelete','/imUserPolicyGroup/delete',30 UNION ALL + SELECT '状态更新','imUserPolicyGroupStatus','/imUserPolicyGroup/status',40 UNION ALL + SELECT '成员列表','imUserPolicyGroupMemberList','/imUserPolicyGroup/memberList',50 UNION ALL + SELECT '添加成员','imUserPolicyGroupMemberAdd','/imUserPolicyGroup/memberAdd',60 UNION ALL + SELECT '移除成员','imUserPolicyGroupMemberRemove','/imUserPolicyGroup/memberRemove',70 +) p WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` m WHERE m.`name`=p.name); + +INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT NULL,@imRootId,'敏感词库','imSensitiveWordIndex','sensitive-word/index','','2','','/imSensitiveWord/list','','/imSensitiveWord/index','1','imManager','0','0','','0','0','0','2',CONCAT('tr_',@imRootId,' '),'90','','1',@now,@now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name`='imSensitiveWordIndex'); +SET @sensitiveWordId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='imSensitiveWordIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT NULL,@sensitiveWordId,p.title,p.name,'','','3','',p.permissions,'','','1','','0','0','','0','0','0','3',CONCAT('tr_',@imRootId,' tr_',@sensitiveWordId,' '),p.sort,'','1',@now,@now +FROM ( + SELECT '敏感词编辑' title,'imSensitiveWordEdit' name,'/imSensitiveWord/edit' permissions,10 sort UNION ALL + SELECT '敏感词删除','imSensitiveWordDelete','/imSensitiveWord/delete',20 UNION ALL + SELECT '状态更新','imSensitiveWordStatus','/imSensitiveWord/status',30 UNION ALL + SELECT '批量导入','imSensitiveWordBatchImport','/imSensitiveWord/batchImport',40 UNION ALL + SELECT '分组列表','imSensitiveWordGroupList','/imSensitiveWord/groupList',50 UNION ALL + SELECT '分组编辑','imSensitiveWordGroupEdit','/imSensitiveWord/groupEdit',60 +) p WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` m WHERE m.`name`=p.name); + +INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT NULL,@imRootId,'安全策略','imSecurityPolicyIndex','security-policy/index','','2','','/imSecurityPolicy/list','','/imSecurityPolicy/index','1','imManager','0','0','','0','0','0','2',CONCAT('tr_',@imRootId,' '),'95','','1',@now,@now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name`='imSecurityPolicyIndex'); +SET @securityPolicyId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='imSecurityPolicyIndex' LIMIT 1); + +INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT NULL,@securityPolicyId,p.title,p.name,'','','3','',p.permissions,'','','1','','0','0','','0','0','0','3',CONCAT('tr_',@imRootId,' tr_',@securityPolicyId,' '),p.sort,'','1',@now,@now +FROM ( + SELECT '策略详情' title,'imSecurityPolicyView' name,'/imSecurityPolicy/view' permissions,10 sort UNION ALL + SELECT '保存草稿','imSecurityPolicySave','/imSecurityPolicy/save',20 UNION ALL + SELECT '发布策略','imSecurityPolicyPublish','/imSecurityPolicy/publish',30 UNION ALL + SELECT '恢复默认','imSecurityPolicyRestoreDefault','/imSecurityPolicy/restoreDefault',40 UNION ALL + SELECT 'IP规则列表','imSecurityPolicyIpRuleList','/imSecurityPolicy/ipRuleList',50 UNION ALL + SELECT 'IP规则编辑','imSecurityPolicyIpRuleEdit','/imSecurityPolicy/ipRuleEdit',60 UNION ALL + SELECT 'IP规则删除','imSecurityPolicyIpRuleDelete','/imSecurityPolicy/ipRuleDelete',70 +) p WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` m WHERE m.`name`=p.name); diff --git a/web/src/api/imDashboard/index.ts b/web/src/api/imDashboard/index.ts new file mode 100644 index 0000000..bcec7ee --- /dev/null +++ b/web/src/api/imDashboard/index.ts @@ -0,0 +1,9 @@ +import { http } from '@/utils/http/axios'; + +export function Overview(params) { + return http.request({ url: '/imDashboard/overview', method: 'GET', params }); +} + +export function Trends(params) { + return http.request({ url: '/imDashboard/trends', method: 'GET', params }); +} diff --git a/web/src/api/imGroupMessage/index.ts b/web/src/api/imGroupMessage/index.ts new file mode 100644 index 0000000..4f762f9 --- /dev/null +++ b/web/src/api/imGroupMessage/index.ts @@ -0,0 +1,17 @@ +import { http, jumpExport } from '@/utils/http/axios'; + +export function List(params) { + return http.request({ url: '/imGroupMessage/list', method: 'GET', params }); +} +export function View(params) { + return http.request({ url: '/imGroupMessage/view', method: 'GET', params }); +} +export function Hide(params) { + return http.request({ url: '/imGroupMessage/hide', method: 'POST', params }); +} +export function Delete(params) { + return http.request({ url: '/imGroupMessage/delete', method: 'POST', params }); +} +export function Export(params) { + jumpExport('/imGroupMessage/export', params); +} diff --git a/web/src/api/imLoginFailureLog/index.ts b/web/src/api/imLoginFailureLog/index.ts new file mode 100644 index 0000000..6cea803 --- /dev/null +++ b/web/src/api/imLoginFailureLog/index.ts @@ -0,0 +1,17 @@ +import { http, jumpExport } from '@/utils/http/axios'; + +export function List(params) { + return http.request({ url: '/imLoginFailureLog/list', method: 'GET', params }); +} +export function View(params) { + return http.request({ url: '/imLoginFailureLog/view', method: 'GET', params }); +} +export function Export(params) { + jumpExport('/imLoginFailureLog/export', params); +} +export function AddBlacklist(params) { + return http.request({ url: '/imLoginFailureLog/addBlacklist', method: 'POST', params }); +} +export function BanUser(params) { + return http.request({ url: '/imLoginFailureLog/banUser', method: 'POST', params }); +} diff --git a/web/src/api/imPrivateMessage/index.ts b/web/src/api/imPrivateMessage/index.ts new file mode 100644 index 0000000..6cd29c4 --- /dev/null +++ b/web/src/api/imPrivateMessage/index.ts @@ -0,0 +1,17 @@ +import { http, jumpExport } from '@/utils/http/axios'; + +export function List(params) { + return http.request({ url: '/imPrivateMessage/list', method: 'GET', params }); +} +export function View(params) { + return http.request({ url: '/imPrivateMessage/view', method: 'GET', params }); +} +export function Hide(params) { + return http.request({ url: '/imPrivateMessage/hide', method: 'POST', params }); +} +export function Delete(params) { + return http.request({ url: '/imPrivateMessage/delete', method: 'POST', params }); +} +export function Export(params) { + jumpExport('/imPrivateMessage/export', params); +} diff --git a/web/src/api/imSecurityPolicy/index.ts b/web/src/api/imSecurityPolicy/index.ts new file mode 100644 index 0000000..afb3c3d --- /dev/null +++ b/web/src/api/imSecurityPolicy/index.ts @@ -0,0 +1,18 @@ +import { http } from '@/utils/http/axios'; + +export const List = (params) => + http.request({ url: '/imSecurityPolicy/list', method: 'GET', params }); +export const View = (params) => + http.request({ url: '/imSecurityPolicy/view', method: 'GET', params }); +export const Save = (params) => + http.request({ url: '/imSecurityPolicy/save', method: 'POST', params }); +export const Publish = (params) => + http.request({ url: '/imSecurityPolicy/publish', method: 'POST', params }); +export const RestoreDefault = (params) => + http.request({ url: '/imSecurityPolicy/restoreDefault', method: 'POST', params }); +export const IPRuleList = (params) => + http.request({ url: '/imSecurityPolicy/ipRuleList', method: 'GET', params }); +export const IPRuleEdit = (params) => + http.request({ url: '/imSecurityPolicy/ipRuleEdit', method: 'POST', params }); +export const IPRuleDelete = (params) => + http.request({ url: '/imSecurityPolicy/ipRuleDelete', method: 'POST', params }); diff --git a/web/src/api/imSensitiveWord/index.ts b/web/src/api/imSensitiveWord/index.ts new file mode 100644 index 0000000..b776d1c --- /dev/null +++ b/web/src/api/imSensitiveWord/index.ts @@ -0,0 +1,16 @@ +import { http } from '@/utils/http/axios'; + +export const List = (params) => + http.request({ url: '/imSensitiveWord/list', method: 'GET', params }); +export const Edit = (params) => + http.request({ url: '/imSensitiveWord/edit', method: 'POST', params }); +export const Delete = (params) => + http.request({ url: '/imSensitiveWord/delete', method: 'POST', params }); +export const Status = (params) => + http.request({ url: '/imSensitiveWord/status', method: 'POST', params }); +export const BatchImport = (params) => + http.request({ url: '/imSensitiveWord/batchImport', method: 'POST', params }); +export const GroupList = (params) => + http.request({ url: '/imSensitiveWord/groupList', method: 'GET', params }); +export const GroupEdit = (params) => + http.request({ url: '/imSensitiveWord/groupEdit', method: 'POST', params }); diff --git a/web/src/api/imUserLoginHistory/index.ts b/web/src/api/imUserLoginHistory/index.ts new file mode 100644 index 0000000..1129361 --- /dev/null +++ b/web/src/api/imUserLoginHistory/index.ts @@ -0,0 +1,8 @@ +import { http, jumpExport } from '@/utils/http/axios'; + +export function List(params) { + return http.request({ url: '/imUserLoginHistory/list', method: 'GET', params }); +} +export function Export(params) { + jumpExport('/imUserLoginHistory/export', params); +} diff --git a/web/src/api/imUserPolicyGroup/index.ts b/web/src/api/imUserPolicyGroup/index.ts new file mode 100644 index 0000000..82807d6 --- /dev/null +++ b/web/src/api/imUserPolicyGroup/index.ts @@ -0,0 +1,18 @@ +import { http } from '@/utils/http/axios'; + +export const List = (params) => + http.request({ url: '/imUserPolicyGroup/list', method: 'GET', params }); +export const View = (params) => + http.request({ url: '/imUserPolicyGroup/view', method: 'GET', params }); +export const Edit = (params) => + http.request({ url: '/imUserPolicyGroup/edit', method: 'POST', params }); +export const Delete = (params) => + http.request({ url: '/imUserPolicyGroup/delete', method: 'POST', params }); +export const Status = (params) => + http.request({ url: '/imUserPolicyGroup/status', method: 'POST', params }); +export const MemberList = (params) => + http.request({ url: '/imUserPolicyGroup/memberList', method: 'GET', params }); +export const MemberAdd = (params) => + http.request({ url: '/imUserPolicyGroup/memberAdd', method: 'POST', params }); +export const MemberRemove = (params) => + http.request({ url: '/imUserPolicyGroup/memberRemove', method: 'POST', params }); diff --git a/web/src/views/imAdminQueryShared.ts b/web/src/views/imAdminQueryShared.ts new file mode 100644 index 0000000..36781f4 --- /dev/null +++ b/web/src/views/imAdminQueryShared.ts @@ -0,0 +1,13 @@ +import { ref } from 'vue'; +import { List as TenantList } from '@/api/imTenant'; + +export const imQueryTenantOptions = ref([]); + +export async function loadImQueryTenantOptions() { + const res = await TenantList({ page: 1, perPage: 100 }); + imQueryTenantOptions.value = (res?.list || []).map((item) => ({ + label: `${item.name}(${item.code})`, + value: item.id, + })); + return imQueryTenantOptions.value; +} diff --git a/web/src/views/imDashboard/index.vue b/web/src/views/imDashboard/index.vue new file mode 100644 index 0000000..c1200c8 --- /dev/null +++ b/web/src/views/imDashboard/index.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/web/src/views/imGroupMessage/index.vue b/web/src/views/imGroupMessage/index.vue new file mode 100644 index 0000000..e4ecace --- /dev/null +++ b/web/src/views/imGroupMessage/index.vue @@ -0,0 +1,4 @@ + + diff --git a/web/src/views/imLoginFailureLog/index.vue b/web/src/views/imLoginFailureLog/index.vue new file mode 100644 index 0000000..b914f24 --- /dev/null +++ b/web/src/views/imLoginFailureLog/index.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/web/src/views/imLoginFailureLog/model.ts b/web/src/views/imLoginFailureLog/model.ts new file mode 100644 index 0000000..a575a7e --- /dev/null +++ b/web/src/views/imLoginFailureLog/model.ts @@ -0,0 +1,92 @@ +import { computed, ref } from 'vue'; +import { FormSchema } from '@/components/Form'; +import { defRangeShortcuts } from '@/utils/dateUtil'; +import { useDictStore } from '@/store/modules/dict'; +import { imQueryTenantOptions } from '@/views/imAdminQueryShared'; + +const dict = useDictStore(); + +export const schemas = ref([ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { + clearable: false, + options: computed(() => imQueryTenantOptions.value), + placeholder: '请选择租户', + }, + }, + { + field: 'userId', + component: 'NInput', + label: '用户ID', + componentProps: { placeholder: '请输入用户ID' }, + }, + { + field: 'account', + component: 'NInput', + label: '账号', + componentProps: { placeholder: '请输入脱敏账号' }, + }, + { + field: 'failureType', + component: 'NSelect', + label: '失败类型', + componentProps: { + clearable: true, + options: dict.getOption('ImLoginFailureTypeOptions'), + placeholder: '全部类型', + }, + }, + { + field: 'platform', + component: 'NSelect', + label: '客户端', + componentProps: { + clearable: true, + options: dict.getOption('ImClientPlatformOptions'), + placeholder: '全部客户端', + }, + }, + { + field: 'ip', + component: 'NInput', + label: 'IP', + componentProps: { placeholder: '请输入IP地址' }, + }, + { + field: 'attemptAt', + component: 'NDatePicker', + label: '尝试时间', + componentProps: { type: 'datetimerange', clearable: true, shortcuts: defRangeShortcuts() }, + }, +]); + +export const columns = [ + { title: 'ID', key: 'id', width: 90 }, + { title: '用户ID', key: 'userId', width: 130 }, + { title: '账号', key: 'accountMask', width: 170 }, + { + title: '失败类型', + key: 'failureType', + width: 140, + render: (row) => dict.getLabel('ImLoginFailureTypeOptions', row.failureType) || row.failureType, + }, + { title: '原因码', key: 'reason', width: 180 }, + { title: 'IP', key: 'ip', width: 140 }, + { title: '地区', key: 'region', width: 150 }, + { + title: '客户端', + key: 'platform', + width: 110, + render: (row) => dict.getLabel('ImClientPlatformOptions', row.platform) || row.platform, + }, + { title: '风险', key: 'riskLevel', width: 90 }, + { title: '风控动作', key: 'riskAction', width: 120 }, + { title: '尝试时间', key: 'attemptAt', width: 180 }, +]; + +export function loadOptions() { + dict.loadOptions(['ImLoginFailureTypeOptions', 'ImClientPlatformOptions']); +} diff --git a/web/src/views/imMessageShared/MessageTable.vue b/web/src/views/imMessageShared/MessageTable.vue new file mode 100644 index 0000000..3c82d3f --- /dev/null +++ b/web/src/views/imMessageShared/MessageTable.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/web/src/views/imMessageShared/model.ts b/web/src/views/imMessageShared/model.ts new file mode 100644 index 0000000..47d32e9 --- /dev/null +++ b/web/src/views/imMessageShared/model.ts @@ -0,0 +1,131 @@ +import { computed, ref } from 'vue'; +import { FormSchema } from '@/components/Form'; +import { defRangeShortcuts } from '@/utils/dateUtil'; +import { useDictStore } from '@/store/modules/dict'; +import { imQueryTenantOptions } from '@/views/imAdminQueryShared'; + +const dict = useDictStore(); + +export function createMessageSchemas(group: boolean) { + const items: FormSchema[] = [ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { + clearable: false, + options: computed(() => imQueryTenantOptions.value), + placeholder: '请选择租户', + }, + }, + { + field: 'topic', + component: 'NInput', + label: group ? '群Topic' : '会话Topic', + componentProps: { placeholder: '请输入Topic' }, + }, + { + field: group ? 'senderUserId' : 'fromUserId', + component: 'NInput', + label: '发送用户', + componentProps: { placeholder: '请输入用户ID' }, + }, + ]; + if (!group) + items.push({ + field: 'toUserId', + component: 'NInput', + label: '接收用户', + componentProps: { placeholder: '请输入用户ID' }, + }); + items.push( + { + field: 'contentType', + component: 'NSelect', + label: '消息类型', + componentProps: { + clearable: true, + options: dict.getOption('ImMessageContentTypeOptions'), + placeholder: '全部类型', + }, + }, + { + field: 'status', + component: 'NSelect', + label: '消息状态', + componentProps: { + clearable: true, + options: dict.getOption('ImMessageStatusOptions'), + placeholder: '全部状态', + }, + }, + { + field: 'keyword', + component: 'NInput', + label: '关键词', + componentProps: { placeholder: '请输入内容关键词' }, + }, + { + field: 'createdAt', + component: 'NDatePicker', + label: '发送时间', + componentProps: { type: 'datetimerange', clearable: true, shortcuts: defRangeShortcuts() }, + } + ); + return ref(items); +} + +export function createMessageColumns(group: boolean) { + const columns: any[] = [ + { title: '消息序号', key: 'seqId', width: 110 }, + { + title: '发送人', + key: 'fromUserId', + width: 190, + render: (row) => + row.senderNickname + ? `${row.senderNickname} / ${row.senderUsername || row.fromUserId}` + : row.senderUsername || row.fromUserId, + }, + ]; + if (group) + columns.push({ + title: '群名称', + key: 'groupName', + width: 190, + render: (row) => + row.groupName ? `${row.groupName} / ${row.groupNo || row.topic}` : row.topic, + }); + else + columns.push({ + title: '接收人', + key: 'toUserId', + width: 190, + render: (row) => + row.receiverNickname + ? `${row.receiverNickname} / ${row.receiverUsername || row.toUserId}` + : row.receiverUsername || row.toUserId, + }); + columns.push( + { + title: '类型', + key: 'contentType', + width: 100, + render: (row) => + dict.getLabel('ImMessageContentTypeOptions', row.contentType) || row.contentType, + }, + { title: '内容', key: 'contentExcerpt', width: 300, ellipsis: { tooltip: true } }, + { + title: '状态', + key: 'status', + width: 110, + render: (row) => dict.getLabel('ImMessageStatusOptions', row.status) || row.status, + }, + { title: '发送时间', key: 'createdAt', width: 180 } + ); + return columns; +} + +export function loadMessageOptions() { + dict.loadOptions(['ImMessageContentTypeOptions', 'ImMessageStatusOptions']); +} diff --git a/web/src/views/imPolicyShared.ts b/web/src/views/imPolicyShared.ts new file mode 100644 index 0000000..9c004dd --- /dev/null +++ b/web/src/views/imPolicyShared.ts @@ -0,0 +1,32 @@ +import { ref } from 'vue'; +import { List as TenantList } from '@/api/imTenant'; + +export const policyTenantOptions = ref([{ label: '全局默认', value: 0 }]); + +export async function loadPolicyTenantOptions(includeGlobal = true) { + const res = await TenantList({ page: 1, perPage: 100 }); + const tenants = (res?.list || []).map((item) => ({ + label: `${item.name}(${item.code})`, + value: item.id, + })); + policyTenantOptions.value = includeGlobal + ? [{ label: '全局默认', value: 0 }, ...tenants] + : tenants; + return policyTenantOptions.value; +} + +export function parseJSON(value: string, fallback: any) { + try { + return value ? JSON.parse(value) : fallback; + } catch { + return fallback; + } +} + +export function stringifyJSON(value: any, fallback = '{}') { + try { + return JSON.stringify(value ?? JSON.parse(fallback)); + } catch { + return fallback; + } +} diff --git a/web/src/views/imPrivateMessage/index.vue b/web/src/views/imPrivateMessage/index.vue new file mode 100644 index 0000000..91db3d0 --- /dev/null +++ b/web/src/views/imPrivateMessage/index.vue @@ -0,0 +1,4 @@ + + diff --git a/web/src/views/imSecurityPolicy/index.vue b/web/src/views/imSecurityPolicy/index.vue new file mode 100644 index 0000000..f4feb69 --- /dev/null +++ b/web/src/views/imSecurityPolicy/index.vue @@ -0,0 +1,568 @@ + + + diff --git a/web/src/views/imSecurityPolicy/model.ts b/web/src/views/imSecurityPolicy/model.ts new file mode 100644 index 0000000..0a67393 --- /dev/null +++ b/web/src/views/imSecurityPolicy/model.ts @@ -0,0 +1,88 @@ +import { computed, ref } from 'vue'; +import { FormSchema } from '@/components/Form'; +import { useDictStore } from '@/store/modules/dict'; +import { policyTenantOptions } from '@/views/imPolicyShared'; + +const dict = useDictStore(); +export const schemas = ref([ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { options: computed(() => policyTenantOptions.value), clearable: false }, + }, + { + field: 'publishStatus', + component: 'NSelect', + label: '发布状态', + componentProps: { + options: computed(() => dict.getOptions('ImSecurityPublishStatusOptions')), + clearable: true, + }, + }, +]); +export const columns = [ + { title: '版本', key: 'policyVersion', width: 90 }, + { title: '策略名称', key: 'policyName', width: 180 }, + { + title: '发布状态', + key: 'publishStatus', + width: 110, + render: (r) => dict.getLabel('ImSecurityPublishStatusOptions', r.publishStatus), + }, + { + title: '允许加好友', + key: 'globalFriendEnabled', + width: 110, + render: (r) => (r.globalFriendEnabled ? '允许' : '禁止'), + }, + { + title: '消息宵禁', + key: 'messageCurfewEnabled', + width: 100, + render: (r) => (r.messageCurfewEnabled ? '启用' : '关闭'), + }, + { title: '发布时间', key: 'publishedAt', width: 180 }, + { title: '更新时间', key: 'updatedAt', width: 180 }, + { title: '备注', key: 'remark', width: 220 }, +]; +export const ipColumns = [ + { + title: '类型', + key: 'ruleType', + width: 100, + render: (r) => (r.ruleType === 'whitelist' ? '白名单' : '黑名单'), + }, + { title: 'IP/CIDR', key: 'ipCidr', width: 180 }, + { + title: '动作', + key: 'action', + width: 110, + render: (r) => dict.getLabel('ImSecurityActionOptions', r.action), + }, + { + title: '风险等级', + key: 'riskLevel', + width: 100, + render: (r) => dict.getLabel('ImRiskLevelOptions', r.riskLevel), + }, + { + title: '状态', + key: 'status', + width: 90, + render: (r) => dict.getLabel('sys_normal_disable', r.status), + }, + { title: '过期时间', key: 'expiresAt', width: 180 }, + { title: '备注', key: 'remark', width: 200 }, +]; +export function loadOptions() { + dict.loadOptions([ + 'ImSecurityPublishStatusOptions', + 'ImCurfewDateTypeOptions', + 'ImCurfewScopeOptions', + 'ImSecurityActionOptions', + 'ImFriendEntryOptions', + 'ImRiskLevelOptions', + 'sys_normal_disable', + ]); +} diff --git a/web/src/views/imSensitiveWord/index.vue b/web/src/views/imSensitiveWord/index.vue new file mode 100644 index 0000000..7bbff1d --- /dev/null +++ b/web/src/views/imSensitiveWord/index.vue @@ -0,0 +1,330 @@ + + + diff --git a/web/src/views/imSensitiveWord/model.ts b/web/src/views/imSensitiveWord/model.ts new file mode 100644 index 0000000..f0ec9ce --- /dev/null +++ b/web/src/views/imSensitiveWord/model.ts @@ -0,0 +1,110 @@ +import { computed, ref } from 'vue'; +import { FormSchema } from '@/components/Form'; +import { useDictStore } from '@/store/modules/dict'; +import { policyTenantOptions } from '@/views/imPolicyShared'; + +const dict = useDictStore(); +export const groupOptions = ref([]); +export const schemas = ref([ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { options: computed(() => policyTenantOptions.value), clearable: false }, + }, + { field: 'word', component: 'NInput', label: '敏感词', componentProps: { clearable: true } }, + { + field: 'groupId', + component: 'NSelect', + label: '分组', + componentProps: { options: computed(() => groupOptions.value), clearable: true }, + }, + { + field: 'matchType', + component: 'NSelect', + label: '匹配类型', + componentProps: { + options: computed(() => dict.getOptions('ImSensitiveMatchTypeOptions')), + clearable: true, + }, + }, + { + field: 'action', + component: 'NSelect', + label: '命中动作', + componentProps: { + options: computed(() => dict.getOptions('ImSensitiveActionOptions')), + clearable: true, + }, + }, + { + field: 'riskLevel', + component: 'NSelect', + label: '风险等级', + componentProps: { + options: computed(() => dict.getOptions('ImRiskLevelOptions')), + clearable: true, + }, + }, + { + field: 'status', + component: 'NSelect', + label: '状态', + componentProps: { + options: computed(() => dict.getOptions('sys_normal_disable')), + clearable: true, + }, + }, +]); +export const columns = [ + { title: '敏感词', key: 'word', width: 180 }, + { title: '分组', key: 'groupName', width: 140 }, + { + title: '匹配类型', + key: 'matchType', + width: 110, + render: (r) => dict.getLabel('ImSensitiveMatchTypeOptions', r.matchType), + }, + { + title: '生效场景', + key: 'sceneJson', + width: 220, + render: (r) => { + try { + return JSON.parse(r.sceneJson || '[]') + .map((v) => dict.getLabel('ImSensitiveSceneOptions', v) || v) + .join('、'); + } catch { + return ''; + } + }, + }, + { + title: '命中动作', + key: 'action', + width: 110, + render: (r) => dict.getLabel('ImSensitiveActionOptions', r.action), + }, + { + title: '风险等级', + key: 'riskLevel', + width: 100, + render: (r) => dict.getLabel('ImRiskLevelOptions', r.riskLevel), + }, + { + title: '状态', + key: 'status', + width: 90, + render: (r) => dict.getLabel('sys_normal_disable', r.status), + }, + { title: '更新时间', key: 'updatedAt', width: 180 }, +]; +export function loadOptions() { + dict.loadOptions([ + 'ImSensitiveMatchTypeOptions', + 'ImSensitiveActionOptions', + 'ImSensitiveSceneOptions', + 'ImRiskLevelOptions', + 'sys_normal_disable', + ]); +} diff --git a/web/src/views/imUserLoginHistory/index.vue b/web/src/views/imUserLoginHistory/index.vue new file mode 100644 index 0000000..da01c74 --- /dev/null +++ b/web/src/views/imUserLoginHistory/index.vue @@ -0,0 +1,95 @@ + + + diff --git a/web/src/views/imUserLoginHistory/model.ts b/web/src/views/imUserLoginHistory/model.ts new file mode 100644 index 0000000..b96fc8d --- /dev/null +++ b/web/src/views/imUserLoginHistory/model.ts @@ -0,0 +1,109 @@ +import { computed, ref } from 'vue'; +import { FormSchema } from '@/components/Form'; +import { defRangeShortcuts } from '@/utils/dateUtil'; +import { useDictStore } from '@/store/modules/dict'; +import { imQueryTenantOptions } from '@/views/imAdminQueryShared'; + +const dict = useDictStore(); + +export const schemas = ref([ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { + clearable: false, + options: computed(() => imQueryTenantOptions.value), + placeholder: '请选择租户', + }, + }, + { + field: 'userId', + component: 'NInput', + label: '用户ID', + componentProps: { placeholder: '请输入用户ID' }, + }, + { + field: 'account', + component: 'NInput', + label: '账号', + componentProps: { placeholder: '请输入手机号/脱敏账号' }, + }, + { + field: 'loginType', + component: 'NSelect', + label: '登录方式', + componentProps: { + clearable: true, + options: dict.getOption('ImLoginTypeOptions'), + placeholder: '全部方式', + }, + }, + { + field: 'eventType', + component: 'NSelect', + label: '事件类型', + componentProps: { + clearable: true, + options: dict.getOption('ImLoginEventTypeOptions'), + placeholder: '全部事件', + }, + }, + { + field: 'platform', + component: 'NSelect', + label: '客户端', + componentProps: { + clearable: true, + options: dict.getOption('ImClientPlatformOptions'), + placeholder: '全部客户端', + }, + }, + { + field: 'ip', + component: 'NInput', + label: 'IP', + componentProps: { placeholder: '请输入IP地址' }, + }, + { + field: 'loginAt', + component: 'NDatePicker', + label: '登录时间', + componentProps: { type: 'datetimerange', clearable: true, shortcuts: defRangeShortcuts() }, + }, +]); + +export const columns = [ + { title: 'ID', key: 'id', width: 90 }, + { title: '用户ID', key: 'userId', width: 130 }, + { title: '用户名', key: 'username', width: 150 }, + { title: '账号', key: 'accountMask', width: 170 }, + { + title: '登录方式', + key: 'loginType', + width: 120, + render: (row) => dict.getLabel('ImLoginTypeOptions', row.loginType) || row.loginType, + }, + { + title: '事件', + key: 'eventType', + width: 120, + render: (row) => dict.getLabel('ImLoginEventTypeOptions', row.eventType) || row.eventType, + }, + { + title: '客户端', + key: 'platform', + width: 110, + render: (row) => dict.getLabel('ImClientPlatformOptions', row.platform) || row.platform, + }, + { title: '版本', key: 'appVersion', width: 100 }, + { title: 'IP', key: 'ip', width: 140 }, + { title: '地区', key: 'region', width: 150 }, + { title: '风险', key: 'riskLevel', width: 90 }, + { title: '登录时间', key: 'loginAt', width: 180 }, + { title: '登出时间', key: 'logoutAt', width: 180 }, +]; + +export function loadOptions() { + dict.loadOptions(['ImLoginTypeOptions', 'ImLoginEventTypeOptions', 'ImClientPlatformOptions']); +} diff --git a/web/src/views/imUserPolicyGroup/index.vue b/web/src/views/imUserPolicyGroup/index.vue new file mode 100644 index 0000000..e6f7c99 --- /dev/null +++ b/web/src/views/imUserPolicyGroup/index.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/web/src/views/imUserPolicyGroup/model.ts b/web/src/views/imUserPolicyGroup/model.ts new file mode 100644 index 0000000..b512ab3 --- /dev/null +++ b/web/src/views/imUserPolicyGroup/model.ts @@ -0,0 +1,64 @@ +import { computed, ref } from 'vue'; +import { FormSchema } from '@/components/Form'; +import { useDictStore } from '@/store/modules/dict'; +import { policyTenantOptions } from '@/views/imPolicyShared'; + +const dict = useDictStore(); + +export const defaultPolicy = { + allow_add_friend: true, + direct_add_friend: false, + max_friend_count: 5000, + allow_qrcode_add_friend: true, + allow_group_member_profile: true, + hide_profile_when_friend_disabled: true, + allow_private_message: true, + allow_group_message: true, + allow_create_group: true, + allow_join_group: true, + allow_upload_file: true, +}; + +export const schemas = ref([ + { + field: 'tenantId', + component: 'NSelect', + label: '租户', + componentProps: { options: computed(() => policyTenantOptions.value), clearable: false }, + }, + { + field: 'keyword', + component: 'NInput', + label: '关键词', + componentProps: { placeholder: '分组编码或名称', clearable: true }, + }, + { + field: 'status', + component: 'NSelect', + label: '状态', + componentProps: { + options: computed(() => dict.getOptions('sys_normal_disable')), + clearable: true, + }, + }, +]); + +export const columns = [ + { title: '分组编码', key: 'groupCode', width: 150 }, + { title: '分组名称', key: 'groupName', width: 160 }, + { title: '优先级', key: 'priority', width: 90 }, + { title: '成员数', key: 'memberCount', width: 90 }, + { + title: '状态', + key: 'status', + width: 90, + render: (row) => dict.getLabel('sys_normal_disable', row.status), + }, + { title: '版本', key: 'version', width: 80 }, + { title: '备注', key: 'remark', width: 220 }, + { title: '更新时间', key: 'updatedAt', width: 180 }, +]; + +export function loadOptions() { + dict.loadOptions(['sys_normal_disable', 'ImPolicyActionOptions', 'ImFriendEntryOptions']); +}