feat: 新增IM联系我们字典配置,优化用户搜索与邀请绑定功能
1. 优化IM用户列表搜索逻辑,支持长数字用户ID精确匹配 2. 修复前端雪花ID精度丢失问题,改用字符串传输用户ID 3. 新增邀请码批量绑定用户功能,支持搜索绑定指定用户 4. 新增字典配置接口,新增联系我们字典SQL脚本 5. 完善字典数据变更事件推送,优化弹窗关闭与页面刷新逻辑
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- IM「联系我们」配置:字典管理新增 im_contact_us 字典类型及默认字典数据
|
||||||
|
-- 用途:移动端通过 im-server 接口 /v1/im/contact-us 读取展示客服邮箱
|
||||||
|
-- 依赖:无需前置 SQL
|
||||||
|
-- 幂等:重复执行不产生重复行
|
||||||
|
-- 说明:邮箱的最终值请在管理后台「系统管理 → 字典管理 → 联系我们」中维护
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
USE hotgo;
|
||||||
|
SET @now := NOW();
|
||||||
|
|
||||||
|
-- 字典类型:联系我们(挂载在系统枚举 sys 下)
|
||||||
|
INSERT INTO `hg_sys_dict_type` (`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||||
|
SELECT (SELECT `id` FROM `hg_sys_dict_type` WHERE `type` = 'sys' LIMIT 1), '联系我们', 'im_contact_us', 0, '移动端联系我们配置', 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `hg_sys_dict_type` WHERE `type` = 'im_contact_us');
|
||||||
|
|
||||||
|
-- 字典数据:客服邮箱
|
||||||
|
INSERT INTO `hg_sys_dict_data` (`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||||
|
SELECT '客服邮箱', 'support@example.com', 'string', 'im_contact_us', '', 1, 10, '移动端展示的客服邮箱', 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `hg_sys_dict_data` WHERE `type` = 'im_contact_us' AND `label` = '客服邮箱');
|
||||||
@@ -62,6 +62,21 @@ type ThirdPartyRes struct {
|
|||||||
Values map[string]string `json:"values" dc:"配置值"`
|
Values map[string]string `json:"values" dc:"配置值"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DictionaryReq struct {
|
||||||
|
g.Meta `path:"/internal/im-config/dictionary/{type}" method:"get" tags:"IM内部配置" summary:"获取字典配置"`
|
||||||
|
Type string `json:"type" in:"path" dc:"字典类型"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DictionaryItemModel struct {
|
||||||
|
Label string `json:"label" dc:"展示名"`
|
||||||
|
Value string `json:"value" dc:"配置值"`
|
||||||
|
Sort int `json:"sort" dc:"排序"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DictionaryRes struct {
|
||||||
|
Items []*DictionaryItemModel `json:"items" dc:"字典项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
type ModerationProviderReq struct {
|
type ModerationProviderReq struct {
|
||||||
g.Meta `path:"/internal/im-config/moderation/provider" method:"get" tags:"IM内部配置" summary:"获取内容审核服务配置"`
|
g.Meta `path:"/internal/im-config/moderation/provider" method:"get" tags:"IM内部配置" summary:"获取内容审核服务配置"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,30 @@ func (c *cConfig) ThirdParty(ctx context.Context, req *api.ThirdPartyReq) (res *
|
|||||||
return &api.ThirdPartyRes{Values: values}, nil
|
return &api.ThirdPartyRes{Values: values}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *cConfig) Dictionary(ctx context.Context, req *api.DictionaryReq) (res *api.DictionaryRes, err error) {
|
||||||
|
dictType := strings.TrimSpace(req.Type)
|
||||||
|
if dictType == "" || dictType == consts.ImInternalSecretDictType {
|
||||||
|
return nil, gerror.New("字典类型不合法")
|
||||||
|
}
|
||||||
|
var rows []*entity.SysDictData
|
||||||
|
if err = dao.SysDictData.Ctx(ctx).
|
||||||
|
Where(dao.SysDictData.Columns().Type, dictType).
|
||||||
|
Where(dao.SysDictData.Columns().Status, consts.StatusEnabled).
|
||||||
|
Order(dao.SysDictData.Columns().Sort + " asc, " + dao.SysDictData.Columns().Id + " desc").
|
||||||
|
Scan(&rows); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "获取IM字典配置失败")
|
||||||
|
}
|
||||||
|
res = &api.DictionaryRes{Items: make([]*api.DictionaryItemModel, 0, len(rows))}
|
||||||
|
for _, row := range rows {
|
||||||
|
res.Items = append(res.Items, &api.DictionaryItemModel{
|
||||||
|
Label: row.Label,
|
||||||
|
Value: row.Value,
|
||||||
|
Sort: row.Sort,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *cConfig) ModerationProvider(ctx context.Context, req *api.ModerationProviderReq) (res *api.ModerationProviderRes, err error) {
|
func (c *cConfig) ModerationProvider(ctx context.Context, req *api.ModerationProviderReq) (res *api.ModerationProviderRes, err error) {
|
||||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, defaultModerationTenantCode)
|
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, defaultModerationTenantCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"hotgo/internal/library/dict"
|
"hotgo/internal/library/dict"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
@@ -30,6 +31,7 @@ func init() {
|
|||||||
|
|
||||||
// Delete 删除
|
// Delete 删除
|
||||||
func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error {
|
func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error {
|
||||||
|
oldTypes := s.getTypesByIds(ctx, in.Id)
|
||||||
if count, err := dao.SysDictData.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType).Count(); err != nil {
|
if count, err := dao.SysDictData.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType).Count(); err != nil {
|
||||||
return gerror.Wrap(err, consts.ErrorORM)
|
return gerror.Wrap(err, consts.ErrorORM)
|
||||||
} else if count > 0 {
|
} else if count > 0 {
|
||||||
@@ -40,6 +42,7 @@ func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp)
|
|||||||
err = gerror.Wrap(err, consts.ErrorORM)
|
err = gerror.Wrap(err, consts.ErrorORM)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.publishDictUpdated(ctx, oldTypes)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +55,13 @@ func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err
|
|||||||
} else if count > 0 {
|
} else if count > 0 {
|
||||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||||
}
|
}
|
||||||
|
oldTypes := s.getTypesByIds(ctx, in.Id)
|
||||||
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataUpdateFields{}).WherePri(in.Id).Data(in).Update()
|
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataUpdateFields{}).WherePri(in.Id).Data(in).Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gerror.Wrap(err, consts.ErrorORM)
|
err = gerror.Wrap(err, consts.ErrorORM)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.publishDictUpdated(ctx, append(oldTypes, in.Type))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +84,33 @@ func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err
|
|||||||
err = gerror.Wrap(err, consts.ErrorORM)
|
err = gerror.Wrap(err, consts.ErrorORM)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.publishDictUpdated(ctx, []string{in.Type})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTypesByIds 查询指定字典数据ID的类型标识,用于变更后失效缓存
|
||||||
|
func (s *sSysDictData) getTypesByIds(ctx context.Context, ids interface{}) []string {
|
||||||
|
columns, err := dao.SysDictData.Ctx(ctx).Where(dao.SysDictData.Columns().Id, ids).Fields(dao.SysDictData.Columns().Type).Array()
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Warningf(ctx, "查询字典类型失败 err:%+v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return g.NewVar(columns).Strings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishDictUpdated 发布字典变更事件,通知 im-server 实时失效字典配置缓存
|
||||||
|
func (s *sSysDictData) publishDictUpdated(ctx context.Context, types []string) {
|
||||||
|
for _, t := range types {
|
||||||
|
t = strings.TrimSpace(t)
|
||||||
|
if t == "" || t == consts.ImInternalSecretDictType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := publishImPolicyEvent(ctx, 0, "*", consts.ImConfigEventDictionaryUpdated, t, 0); err != nil {
|
||||||
|
g.Log().Warningf(ctx, "发布字典变更事件失败 type:%s err:%+v", t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// List 获取列表
|
// List 获取列表
|
||||||
func (s *sSysDictData) List(ctx context.Context, in *sysin.DictDataListInp) (list []*sysin.DictDataListModel, totalCount int, err error) {
|
func (s *sSysDictData) List(ctx context.Context, in *sysin.DictDataListInp) (list []*sysin.DictDataListModel, totalCount int, err error) {
|
||||||
mod := dao.SysDictData.Ctx(ctx).WhereNot(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType)
|
mod := dao.SysDictData.Ctx(ctx).WhereNot(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType)
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func (s *sSysImInvite) Create(ctx context.Context, in *sysin.ImInviteCreateInp)
|
|||||||
expiresAt := in.ExpiresAt.Time.UTC().Round(time.Millisecond)
|
expiresAt := in.ExpiresAt.Time.UTC().Round(time.Millisecond)
|
||||||
var item imInviteCodeResponse
|
var item imInviteCodeResponse
|
||||||
if key := strings.TrimSpace(in.IdempotencyKey); key != "" {
|
if key := strings.TrimSpace(in.IdempotencyKey); key != "" {
|
||||||
batch, codes, createErr := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, 1, expiresAt, in.Remark, key)
|
batch, codes, createErr := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, 1, expiresAt, in.Remark, key, 0, "")
|
||||||
if createErr != nil {
|
if createErr != nil {
|
||||||
err = createErr
|
err = createErr
|
||||||
} else if len(codes) > 0 {
|
} else if len(codes) > 0 {
|
||||||
@@ -150,15 +150,27 @@ func (s *sSysImInvite) BatchCreate(ctx context.Context, in *sysin.ImInviteBatchC
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var boundUserLabel string
|
||||||
|
if in.UserId > 0 {
|
||||||
|
tenant, terr := resolveImAdminTenant(ctx, scope.TenantId)
|
||||||
|
if terr != nil {
|
||||||
|
return nil, terr
|
||||||
|
}
|
||||||
|
label, lerr := inviteBoundUserLabel(ctx, tenant, in.UserId)
|
||||||
|
if lerr != nil {
|
||||||
|
return nil, lerr
|
||||||
|
}
|
||||||
|
boundUserLabel = label
|
||||||
|
}
|
||||||
key := strings.TrimSpace(in.IdempotencyKey)
|
key := strings.TrimSpace(in.IdempotencyKey)
|
||||||
batch, _, err := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, in.Quantity, in.ExpiresAt.Time.UTC().Round(time.Millisecond), in.Remark, key)
|
batch, _, err := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, in.Quantity, in.ExpiresAt.Time.UTC().Round(time.Millisecond), in.Remark, key, in.UserId, boundUserLabel)
|
||||||
result := "success"
|
result := "success"
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result = "failed"
|
result = "failed"
|
||||||
} else {
|
} else {
|
||||||
res = convertInviteBatch(batch)
|
res = convertInviteBatch(batch)
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", batchID(batch), result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark})
|
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", batchID(batch), result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark, "boundUserId": in.UserId})
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +370,7 @@ func createSingleInvite(ctx context.Context, db gdb.DB, tenantID, operatorID int
|
|||||||
return imInviteCodeResponse{}, gerror.New("无法生成不重复的邀请码,请重试")
|
return imInviteCodeResponse{}, gerror.New("无法生成不重复的邀请码,请重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int64, quantity int, expiresAt time.Time, remark, key string) (*imInviteBatchResponse, []imInviteCodeResponse, error) {
|
func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int64, quantity int, expiresAt time.Time, remark, key string, boundUserID int64, boundUserLabel string) (*imInviteBatchResponse, []imInviteCodeResponse, error) {
|
||||||
if existing, err := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); err == nil {
|
if existing, err := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); err == nil {
|
||||||
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
|
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
|
||||||
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
|
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
|
||||||
@@ -402,11 +414,16 @@ func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int6
|
|||||||
}
|
}
|
||||||
for i := range codes {
|
for i := range codes {
|
||||||
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_codes
|
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_codes
|
||||||
(id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at)
|
(id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at,bound_user_id,bound_user_label)
|
||||||
VALUES(?,?,?,?,'active',?,0,0,NULL,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now); txErr != nil {
|
VALUES(?,?,?,?,'active',?,0,0,NULL,?,?,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now, boundUserID, boundUserLabel); txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
codes[i].TenantID, codes[i].Status, codes[i].ExpiresAt, codes[i].CreatedBy, codes[i].Remark, codes[i].CreatedAt = tenantID, "active", expiresAt, operatorID, remark, now
|
codes[i].TenantID, codes[i].Status, codes[i].ExpiresAt, codes[i].CreatedBy, codes[i].Remark, codes[i].CreatedAt = tenantID, "active", expiresAt, operatorID, remark, now
|
||||||
|
if boundUserID > 0 {
|
||||||
|
id := boundUserID
|
||||||
|
codes[i].BoundUserID = &id
|
||||||
|
codes[i].BoundUserLabel = boundUserLabel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -206,8 +206,13 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
|
|||||||
values.Set("user_ids", strings.Join(userIds, ","))
|
values.Set("user_ids", strings.Join(userIds, ","))
|
||||||
}
|
}
|
||||||
if in.UserId != "" {
|
if in.UserId != "" {
|
||||||
|
// 长数字按用户ID(u.id)精确匹配,短数字按后台展示编号(account_no)匹配
|
||||||
|
if len(in.UserId) >= 12 {
|
||||||
|
values.Set("user_id", in.UserId)
|
||||||
|
} else {
|
||||||
values.Set("account_no", in.UserId)
|
values.Set("account_no", in.UserId)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if in.Nickname != "" {
|
if in.Nickname != "" {
|
||||||
values.Set("nickname", in.Nickname)
|
values.Set("nickname", in.Nickname)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ type ImInviteBatchCreateInp struct {
|
|||||||
ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"`
|
ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"`
|
||||||
Remark string `json:"remark" dc:"备注"`
|
Remark string `json:"remark" dc:"备注"`
|
||||||
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||||
|
UserId int64 `json:"userId" dc:"绑定C端用户ID,非必填,>0时为批次内所有邀请码绑定该用户"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) {
|
func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) {
|
||||||
@@ -92,6 +93,9 @@ func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) {
|
|||||||
if len(in.IdempotencyKey) == 0 || len(in.IdempotencyKey) > 128 {
|
if len(in.IdempotencyKey) == 0 || len(in.IdempotencyKey) > 128 {
|
||||||
return gerror.New("批量生成必须提供不超过128个字符的幂等键")
|
return gerror.New("批量生成必须提供不超过128个字符的幂等键")
|
||||||
}
|
}
|
||||||
|
if in.UserId < 0 {
|
||||||
|
return gerror.New("绑定用户ID不合法")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
type ImUserListInp struct {
|
type ImUserListInp struct {
|
||||||
form.PageReq
|
form.PageReq
|
||||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||||
UserId string `json:"userId" dc:"后台展示编号"`
|
UserId string `json:"userId" dc:"用户ID(长数字)或后台展示编号"`
|
||||||
GroupId int64 `json:"groupId" dc:"用户策略分组ID"`
|
GroupId int64 `json:"groupId" dc:"用户策略分组ID"`
|
||||||
Keyword string `json:"keyword" dc:"用户名、昵称、手机号或编号"`
|
Keyword string `json:"keyword" dc:"用户名、昵称、手机号或编号"`
|
||||||
Nickname string `json:"nickname" dc:"昵称"`
|
Nickname string `json:"nickname" dc:"昵称"`
|
||||||
|
|||||||
@@ -132,7 +132,8 @@
|
|||||||
}
|
}
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
await BindUser({ tenantId: tenantId.value, id: inviteId.value, userId: Number(selectedUserId.value) });
|
// 雪花ID超出 JS Number 安全整数范围,保持字符串传输避免精度丢失
|
||||||
|
await BindUser({ tenantId: tenantId.value, id: inviteId.value, userId: selectedUserId.value });
|
||||||
message.success('绑定成功');
|
message.success('绑定成功');
|
||||||
showModal.value = false;
|
showModal.value = false;
|
||||||
emit('reload');
|
emit('reload');
|
||||||
|
|||||||
@@ -4,6 +4,23 @@
|
|||||||
<n-form-item v-if="isBatch" label="生成数量" path="quantity">
|
<n-form-item v-if="isBatch" label="生成数量" path="quantity">
|
||||||
<n-input-number v-model:value="formValue.quantity" :min="1" :max="1000" />
|
<n-input-number v-model:value="formValue.quantity" :min="1" :max="1000" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item v-if="isBatch" label="绑定用户" path="boundUserId">
|
||||||
|
<div class="bind-user-row">
|
||||||
|
<n-input
|
||||||
|
v-model:value="bindUserInput"
|
||||||
|
placeholder="输入用户ID/编号(非必填)"
|
||||||
|
clearable
|
||||||
|
:disabled="!!boundUser"
|
||||||
|
@keydown.enter="searchUser"
|
||||||
|
/>
|
||||||
|
<n-button :loading="searching" :disabled="!!boundUser" @click="searchUser">搜索</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="boundUser" class="bound-user-tag">
|
||||||
|
<n-avatar :size="28" :src="boundUser.avatar || defaultAvatar" />
|
||||||
|
<span class="bound-user-name">{{ boundUser.nickname || boundUser.username || boundUser.phone || '--' }}({{ boundUser.userId }})</span>
|
||||||
|
<n-button text type="error" @click="clearBoundUser">清除</n-button>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
<n-form-item label="到期时间" path="expiresAt">
|
<n-form-item label="到期时间" path="expiresAt">
|
||||||
<n-date-picker v-model:value="formValue.expiresAt" type="datetime" clearable />
|
<n-date-picker v-model:value="formValue.expiresAt" type="datetime" clearable />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -24,7 +41,9 @@
|
|||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { BatchCreate, Create } from '@/api/im/registrationInvite';
|
import { BatchCreate, Create } from '@/api/im/registrationInvite';
|
||||||
|
import { List as UserList } from '@/api/imUser';
|
||||||
import { localRfc3339 } from './model';
|
import { localRfc3339 } from './model';
|
||||||
|
import defaultAvatar from '@/assets/images/schoolboy.png';
|
||||||
|
|
||||||
const emit = defineEmits(['reload']);
|
const emit = defineEmits(['reload']);
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
@@ -33,11 +52,15 @@
|
|||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const tenantId = ref(0);
|
const tenantId = ref(0);
|
||||||
const isBatch = ref(false);
|
const isBatch = ref(false);
|
||||||
|
const bindUserInput = ref('');
|
||||||
|
const searching = ref(false);
|
||||||
|
const boundUser = ref<any>(null);
|
||||||
const formValue = reactive({
|
const formValue = reactive({
|
||||||
quantity: 100,
|
quantity: 100,
|
||||||
expiresAt: Date.now() + 7 * 24 * 3600 * 1000,
|
expiresAt: Date.now() + 7 * 24 * 3600 * 1000,
|
||||||
remark: '',
|
remark: '',
|
||||||
idempotencyKey: '',
|
idempotencyKey: '',
|
||||||
|
boundUserId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -52,6 +75,9 @@
|
|||||||
formValue.expiresAt = Date.now() + 7 * 24 * 3600 * 1000;
|
formValue.expiresAt = Date.now() + 7 * 24 * 3600 * 1000;
|
||||||
formValue.remark = '';
|
formValue.remark = '';
|
||||||
formValue.idempotencyKey = crypto.randomUUID();
|
formValue.idempotencyKey = crypto.randomUUID();
|
||||||
|
formValue.boundUserId = '';
|
||||||
|
bindUserInput.value = '';
|
||||||
|
boundUser.value = null;
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,17 +85,61 @@
|
|||||||
showModal.value = false;
|
showModal.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function searchUser() {
|
||||||
|
const keyword = bindUserInput.value.trim();
|
||||||
|
if (!keyword) {
|
||||||
|
message.warning('请输入用户ID/编号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searching.value = true;
|
||||||
|
try {
|
||||||
|
const res = await UserList({
|
||||||
|
tenantId: tenantId.value,
|
||||||
|
userId: keyword,
|
||||||
|
keyword: '',
|
||||||
|
page: 1,
|
||||||
|
perPage: 1,
|
||||||
|
});
|
||||||
|
const user = res?.list?.[0];
|
||||||
|
if (!user) {
|
||||||
|
message.warning('未找到该用户,将不绑定用户');
|
||||||
|
boundUser.value = null;
|
||||||
|
formValue.boundUserId = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boundUser.value = user;
|
||||||
|
// 雪花ID超出 JS Number 安全整数范围,必须保持字符串传输,避免精度丢失
|
||||||
|
formValue.boundUserId = String(user.userId || '');
|
||||||
|
message.success('已选中用户');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error?.toString() || '用户查询失败');
|
||||||
|
boundUser.value = null;
|
||||||
|
formValue.boundUserId = '';
|
||||||
|
} finally {
|
||||||
|
searching.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBoundUser() {
|
||||||
|
boundUser.value = null;
|
||||||
|
formValue.boundUserId = '';
|
||||||
|
bindUserInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
await formRef.value?.validate();
|
await formRef.value?.validate();
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params: any = {
|
||||||
tenantId: tenantId.value,
|
tenantId: tenantId.value,
|
||||||
expiresAt: localRfc3339(formValue.expiresAt),
|
expiresAt: localRfc3339(formValue.expiresAt),
|
||||||
remark: formValue.remark,
|
remark: formValue.remark,
|
||||||
idempotencyKey: formValue.idempotencyKey,
|
idempotencyKey: formValue.idempotencyKey,
|
||||||
};
|
};
|
||||||
if (isBatch.value) {
|
if (isBatch.value) {
|
||||||
|
if (formValue.boundUserId) {
|
||||||
|
params.userId = formValue.boundUserId;
|
||||||
|
}
|
||||||
await BatchCreate({ ...params, quantity: formValue.quantity });
|
await BatchCreate({ ...params, quantity: formValue.quantity });
|
||||||
} else {
|
} else {
|
||||||
await Create(params);
|
await Create(params);
|
||||||
@@ -87,3 +157,22 @@
|
|||||||
defineExpose({ openModal });
|
defineExpose({ openModal });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.bind-user-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bound-user-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bound-user-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -515,6 +515,8 @@
|
|||||||
windowMinutes: lockoutForm.value.windowMinutes,
|
windowMinutes: lockoutForm.value.windowMinutes,
|
||||||
});
|
});
|
||||||
message.success('已保存,配置即时生效');
|
message.success('已保存,配置即时生效');
|
||||||
|
policyVisible.value = false;
|
||||||
|
reloadAll();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e.message || '保存失败');
|
message.error(e.message || '保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user