diff --git a/server/api/admin/iminvite/im_invite.go b/server/api/admin/iminvite/im_invite.go
index 13116c5..45ebe03 100644
--- a/server/api/admin/iminvite/im_invite.go
+++ b/server/api/admin/iminvite/im_invite.go
@@ -51,6 +51,13 @@ type RevokeReq struct {
type RevokeRes struct{}
+type BindUserReq struct {
+ g.Meta `path:"/imInvite/bindUser" method:"post" tags:"IM邀请码" summary:"绑定邀请码用户"`
+ sysin.ImInviteBindUserInp
+}
+
+type BindUserRes struct{}
+
type BatchRevokeReq struct {
g.Meta `path:"/imInvite/batchRevoke" method:"post" tags:"IM邀请码" summary:"批量撤销IM注册邀请码"`
sysin.ImInviteBatchRevokeInp
diff --git a/server/internal/controller/admin/sys/im_invite.go b/server/internal/controller/admin/sys/im_invite.go
index f9761dc..3968c65 100644
--- a/server/internal/controller/admin/sys/im_invite.go
+++ b/server/internal/controller/admin/sys/im_invite.go
@@ -60,6 +60,11 @@ func (c *cImInvite) Revoke(ctx context.Context, req *iminvite.RevokeReq) (res *i
return
}
+func (c *cImInvite) BindUser(ctx context.Context, req *iminvite.BindUserReq) (res *iminvite.BindUserRes, err error) {
+ err = service.SysImInvite().BindUser(ctx, &req.ImInviteBindUserInp)
+ return
+}
+
func (c *cImInvite) BatchRevoke(ctx context.Context, req *iminvite.BatchRevokeReq) (res *iminvite.BatchRevokeRes, err error) {
data, err := service.SysImInvite().BatchRevoke(ctx, &req.ImInviteBatchRevokeInp)
if err != nil {
diff --git a/server/internal/logic/sys/im_invite.go b/server/internal/logic/sys/im_invite.go
index 2b8075c..d2f93e5 100644
--- a/server/internal/logic/sys/im_invite.go
+++ b/server/internal/logic/sys/im_invite.go
@@ -7,8 +7,10 @@ import (
"errors"
"fmt"
"hotgo/internal/library/imtenant"
+ "hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
+ "net/url"
"strconv"
"strings"
"time"
@@ -44,18 +46,21 @@ type imInviteListRequest struct {
}
type imInviteCodeResponse struct {
- ID int64 `json:"id"`
- TenantID int64 `json:"tenant_id"`
- TenantName string `json:"tenant_name"`
- BatchID *int64 `json:"batch_id"`
- Code string `json:"code"`
- Status string `json:"status"`
- ExpiresAt time.Time `json:"expires_at"`
- UsageCount int `json:"usage_count"`
- LastUsedAt *time.Time `json:"last_used_at"`
- CreatedBy int64 `json:"created_by"`
- Remark string `json:"remark"`
- CreatedAt time.Time `json:"created_at"`
+ ID int64 `json:"id"`
+ TenantID int64 `json:"tenant_id"`
+ TenantName string `json:"tenant_name"`
+ BatchID *int64 `json:"batch_id"`
+ Code string `json:"code"`
+ Status string `json:"status"`
+ ExpiresAt time.Time `json:"expires_at"`
+ MaxUses int `json:"max_uses"`
+ UsageCount int `json:"usage_count"`
+ BoundUserID *int64 `json:"bound_user_id"`
+ BoundUserLabel string `json:"bound_user_label"`
+ LastUsedAt *time.Time `json:"last_used_at"`
+ CreatedBy int64 `json:"created_by"`
+ Remark string `json:"remark"`
+ CreatedAt time.Time `json:"created_at"`
}
type imInviteBatchResponse struct {
@@ -202,6 +207,47 @@ func (s *sSysImInvite) Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp)
return err
}
+func (s *sSysImInvite) BindUser(ctx context.Context, in *sysin.ImInviteBindUserInp) (err error) {
+ if err = in.Filter(ctx); err != nil {
+ return err
+ }
+ scope, db, err := inviteDB(ctx, in.TenantId)
+ if err != nil {
+ return err
+ }
+ id, err := sysin.ParseOptionalInt64(in.Id)
+ if err != nil || id <= 0 {
+ return gerror.New("邀请码ID不能为空")
+ }
+ tenant, err := resolveImAdminTenant(ctx, scope.TenantId)
+ if err != nil {
+ return err
+ }
+ label, err := inviteBoundUserLabel(ctx, tenant, in.UserId)
+ if err != nil {
+ return err
+ }
+ now := time.Now().UTC().Round(time.Millisecond)
+ result, err := db.Exec(ctx, `UPDATE im_registration_invite_codes
+ SET bound_user_id=?,bound_user_label=?,updated_at=?
+ WHERE tenant_id=? AND id=? AND status='active' AND usage_count=0 AND (bound_user_id IS NULL OR bound_user_id=0)`,
+ in.UserId, label, now, scope.TenantId, id)
+ if err == nil {
+ if affected, _ := result.RowsAffected(); affected == 0 {
+ err = gerror.New("邀请码不存在、已使用、已绑定或不可绑定")
+ }
+ }
+ auditResult := "success"
+ if err != nil {
+ auditResult = "failed"
+ }
+ _ = writeImAdminAudit(ctx, scope.TenantId, "invite_bind_user", "invite_code", id, auditResult, g.Map{"user_id": in.UserId})
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
func (s *sSysImInvite) BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error) {
scope, db, err := inviteDB(ctx, in.TenantId)
if err != nil {
@@ -251,14 +297,14 @@ func (s *sSysImInvite) Export(ctx context.Context, in *sysin.ImInviteListInp) (e
request.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=registration-invite-codes-%s.csv", time.Now().Format("20060102-150405")))
_, _ = request.Response.Writer.Write([]byte{0xef, 0xbb, 0xbf})
writer := csv.NewWriter(request.Response.Writer)
- _ = writer.Write([]string{"tenant_id", "tenant_name", "code", "status", "batch_id", "expires_at", "usage_count", "last_used_at", "created_by", "created_at", "remark"})
+ _ = writer.Write([]string{"tenant_id", "tenant_name", "code", "status", "expires_at", "usage_count", "available_uses", "last_used_at", "created_by", "created_at", "remark"})
for offset := 0; ; offset += 100 {
items, total, queryErr := queryInviteCodes(ctx, db, req, 100, offset)
if queryErr != nil {
return gerror.Wrap(queryErr, "导出邀请码失败")
}
for _, item := range items {
- _ = writer.Write([]string{strconv.FormatInt(item.TenantID, 10), item.TenantName, item.Code, item.Status, formatOptionalInt64(item.BatchID), item.ExpiresAt.Format(time.RFC3339), strconv.Itoa(item.UsageCount), formatTime(item.LastUsedAt), strconv.FormatInt(item.CreatedBy, 10), item.CreatedAt.Format(time.RFC3339), item.Remark})
+ _ = writer.Write([]string{strconv.FormatInt(item.TenantID, 10), item.TenantName, item.Code, item.Status, item.ExpiresAt.Format(time.RFC3339), strconv.Itoa(item.UsageCount), formatInviteAvailableUses(item.MaxUses, item.UsageCount), formatTime(item.LastUsedAt), strconv.FormatInt(item.CreatedBy, 10), item.CreatedAt.Format(time.RFC3339), item.Remark})
}
writer.Flush()
if writer.Error() != nil {
@@ -300,10 +346,10 @@ func createSingleInvite(ctx context.Context, db gdb.DB, tenantID, operatorID int
}
now := time.Now().UTC().Round(time.Millisecond)
_, err = db.Exec(ctx, `INSERT INTO im_registration_invite_codes
- (id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at)
- VALUES(?,?,NULL,?,'active',?,0,NULL,?,?,?,?)`, id, tenantID, code, expiresAt, operatorID, remark, now, now)
+ (id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at)
+ VALUES(?,?,NULL,?,'active',?,1,0,NULL,?,?,?,?)`, id, tenantID, code, expiresAt, operatorID, remark, now, now)
if err == nil {
- return imInviteCodeResponse{ID: id, TenantID: tenantID, Code: code, Status: "active", ExpiresAt: expiresAt, CreatedBy: operatorID, Remark: remark, CreatedAt: now}, nil
+ return imInviteCodeResponse{ID: id, TenantID: tenantID, Code: code, Status: "active", ExpiresAt: expiresAt, MaxUses: 1, CreatedBy: operatorID, Remark: remark, CreatedAt: now}, nil
}
if !isDuplicateError(err) {
return imInviteCodeResponse{}, err
@@ -356,8 +402,8 @@ func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int6
}
for i := range codes {
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_codes
- (id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at)
- VALUES(?,?,?,?,'active',?,0,NULL,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now); txErr != nil {
+ (id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at)
+ VALUES(?,?,?,?,'active',?,0,0,NULL,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now); txErr != nil {
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
@@ -400,7 +446,7 @@ func queryInviteCodes(ctx context.Context, db gdb.DB, req imInviteListRequest, l
}
queryArgs := append(append([]any{}, args...), limit, offset)
items := make([]imInviteCodeResponse, 0)
- err = db.Raw(`SELECT c.id,c.tenant_id,t.name tenant_name,c.batch_id,c.code,c.status,c.expires_at,c.created_by,c.remark,c.created_at,
+ err = db.Raw(`SELECT c.id,c.tenant_id,t.name tenant_name,c.batch_id,c.code,c.status,c.expires_at,c.max_uses,c.bound_user_id,c.bound_user_label,c.created_by,c.remark,c.created_at,
c.usage_count,c.last_used_at
FROM im_registration_invite_codes c
LEFT JOIN im_tenant t ON t.id=c.tenant_id
@@ -521,10 +567,45 @@ func convertInviteCodes(items []imInviteCodeResponse) []*sysin.ImInviteCodeModel
func convertInviteCode(item imInviteCodeResponse) *sysin.ImInviteCodeModel {
return &sysin.ImInviteCodeModel{Id: strconv.FormatInt(item.ID, 10), BatchId: formatOptionalInt64(item.BatchID), Code: item.Code,
TenantId: strconv.FormatInt(item.TenantID, 10), TenantName: item.TenantName,
- Status: item.Status, ExpiresAt: gtime.New(item.ExpiresAt), UsageCount: item.UsageCount, LastUsedAt: convertTime(item.LastUsedAt),
+ Status: item.Status, ExpiresAt: gtime.New(item.ExpiresAt), MaxUses: item.MaxUses, UsageCount: item.UsageCount,
+ BoundUserId: formatOptionalInt64(item.BoundUserID), BoundUserLabel: item.BoundUserLabel, LastUsedAt: convertTime(item.LastUsedAt),
CreatedBy: strconv.FormatInt(item.CreatedBy, 10), Remark: item.Remark, CreatedAt: gtime.New(item.CreatedAt)}
}
+func inviteBoundUserLabel(ctx context.Context, tenant *entity.ImTenant, userID int64) (string, error) {
+ if tenant == nil || tenant.Code == "" || userID <= 0 {
+ return "", gerror.New("绑定用户不合法")
+ }
+ values := addPageQuery(url.Values{}, 1, 1)
+ values.Set("user_ids", strconv.FormatInt(userID, 10))
+ users, _, err := imAdminQueryList[sysin.ImUserListModel](ctx, tenant,
+ "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users",
+ values,
+ map[string]string{
+ "public": "profileJson",
+ "trusted": "trustedJson",
+ "access": "accessJson",
+ },
+ )
+ if err != nil {
+ return "", err
+ }
+ if len(users) == 0 {
+ return "", gerror.New("绑定用户不存在")
+ }
+ user := users[0]
+ if user.Nickname != "" {
+ return fmt.Sprintf("%s(%s)", user.Nickname, user.UserId), nil
+ }
+ if user.Username != "" {
+ return fmt.Sprintf("%s(%s)", user.Username, user.UserId), nil
+ }
+ if user.Phone != "" {
+ return fmt.Sprintf("%s(%s)", user.Phone, user.UserId), nil
+ }
+ return user.UserId, nil
+}
+
func convertInviteBatch(item *imInviteBatchResponse) *sysin.ImInviteBatchModel {
if item == nil {
return nil
@@ -564,6 +645,17 @@ func formatTime(value *time.Time) string {
return value.Format(time.RFC3339)
}
+func formatInviteAvailableUses(maxUses, usageCount int) string {
+ if maxUses <= 0 {
+ return "不限制"
+ }
+ available := maxUses - usageCount
+ if available < 0 {
+ available = 0
+ }
+ return strconv.Itoa(available)
+}
+
func batchID(batch *imInviteBatchResponse) int64 {
if batch == nil {
return 0
diff --git a/server/internal/model/input/sysin/im_invite.go b/server/internal/model/input/sysin/im_invite.go
index 48df509..b7cb29f 100644
--- a/server/internal/model/input/sysin/im_invite.go
+++ b/server/internal/model/input/sysin/im_invite.go
@@ -105,24 +105,43 @@ type ImInviteRevokeInp struct {
Id string `json:"id" v:"required#邀请码ID不能为空" dc:"邀请码ID"`
}
+type ImInviteBindUserInp struct {
+ TenantId int64 `json:"tenantId" dc:"IM租户ID"`
+ Id string `json:"id" v:"required#邀请码ID不能为空" dc:"邀请码ID"`
+ UserId int64 `json:"userId" v:"required|min:1#绑定用户不能为空|绑定用户不合法" dc:"绑定C端用户ID"`
+}
+
+func (in *ImInviteBindUserInp) Filter(ctx context.Context) (err error) {
+ if _, err = ParseOptionalInt64(in.Id); err != nil {
+ return gerror.New("邀请码ID不能为空")
+ }
+ if in.UserId <= 0 {
+ return gerror.New("绑定用户不能为空")
+ }
+ return nil
+}
+
type ImInviteBatchRevokeInp struct {
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
BatchId string `json:"batchId" v:"required#批次ID不能为空" dc:"批次ID"`
}
type ImInviteCodeModel struct {
- Id string `json:"id" dc:"ID"`
- TenantId string `json:"tenantId" dc:"IM租户ID"`
- TenantName string `json:"tenantName" dc:"IM租户名称"`
- BatchId string `json:"batchId" dc:"批次ID"`
- Code string `json:"code" dc:"邀请码"`
- Status string `json:"status" dc:"状态"`
- ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"`
- UsageCount int `json:"usageCount" dc:"累计使用次数"`
- LastUsedAt *gtime.Time `json:"lastUsedAt" dc:"最后使用时间"`
- CreatedBy string `json:"createdBy" dc:"创建人"`
- Remark string `json:"remark" dc:"备注"`
- CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
+ Id string `json:"id" dc:"ID"`
+ TenantId string `json:"tenantId" dc:"IM租户ID"`
+ TenantName string `json:"tenantName" dc:"IM租户名称"`
+ BatchId string `json:"batchId" dc:"批次ID"`
+ Code string `json:"code" dc:"邀请码"`
+ Status string `json:"status" dc:"状态"`
+ ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"`
+ MaxUses int `json:"maxUses" dc:"最大可用次数,0表示不限制"`
+ UsageCount int `json:"usageCount" dc:"累计使用次数"`
+ BoundUserId string `json:"boundUserId" dc:"绑定用户ID"`
+ BoundUserLabel string `json:"boundUserLabel" dc:"绑定用户展示名"`
+ LastUsedAt *gtime.Time `json:"lastUsedAt" dc:"最后使用时间"`
+ CreatedBy string `json:"createdBy" dc:"创建人"`
+ Remark string `json:"remark" dc:"备注"`
+ CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
}
type ImInviteBatchModel struct {
diff --git a/server/internal/service/sys.go b/server/internal/service/sys.go
index 968c92e..b358819 100644
--- a/server/internal/service/sys.go
+++ b/server/internal/service/sys.go
@@ -209,6 +209,8 @@ type (
BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error)
// Revoke 撤销邀请码
Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error)
+ // BindUser 绑定邀请码注册后自动添加的C端用户
+ BindUser(ctx context.Context, in *sysin.ImInviteBindUserInp) (err error)
// BatchRevoke 批量撤销邀请码
BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error)
// Export 导出邀请码
diff --git a/server/storage/data/generate/im_feedback_config_menu.sql b/server/storage/data/generate/im_feedback_config_menu.sql
index a9753c8..bbfef77 100644
--- a/server/storage/data/generate/im_feedback_config_menu.sql
+++ b/server/storage/data/generate/im_feedback_config_menu.sql
@@ -8,14 +8,14 @@ WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'workbenchManager
SET @workbenchRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'workbenchManager' LIMIT 1);
INSERT INTO `hg_admin_menu` (`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 @workbenchRootId, '意见反馈配置', 'imFeedbackConfigIndex', 'feedback-config/index', '', 2, '', '/imFeedbackConfig/get', '', '/imFeedbackConfig/index', 1, 'workbenchManager', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @workbenchRootId, ' '), 30, '', 1, @now, @now
+SELECT @workbenchRootId, '意见反馈配置', 'imFeedbackConfigIndex', 'feedback-config/index', '', 2, '', '/imFeedbackConfig/get', '', '/imFeedbackConfig/index', 1, 'workbenchManager', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @workbenchRootId, ' '), 40, '', 1, @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex');
SET @imFeedbackConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex' LIMIT 1);
UPDATE `hg_admin_menu`
SET `pid`=@workbenchRootId, `path`='feedback-config/index', `active_menu`='workbenchManager',
- `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=30,
+ `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=40,
`status`=1, `hidden`=0, `updated_at`=@now
WHERE `id`=@imFeedbackConfigId;
diff --git a/server/storage/data/generate/im_mailbox_config_menu.sql b/server/storage/data/generate/im_mailbox_config_menu.sql
index 9a77336..e13044b 100644
--- a/server/storage/data/generate/im_mailbox_config_menu.sql
+++ b/server/storage/data/generate/im_mailbox_config_menu.sql
@@ -8,14 +8,14 @@ WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'workbenchManager
SET @workbenchRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'workbenchManager' LIMIT 1);
INSERT INTO `hg_admin_menu` (`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 @workbenchRootId, '总经理信箱配置', 'imMailboxConfigIndex', 'mailbox-config/index', '', 2, '', '/imMailboxConfig/get', '', '/imMailboxConfig/index', 1, 'workbenchManager', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @workbenchRootId, ' '), 20, '', 1, @now, @now
+SELECT @workbenchRootId, '总经理信箱配置', 'imMailboxConfigIndex', 'mailbox-config/index', '', 2, '', '/imMailboxConfig/get', '', '/imMailboxConfig/index', 1, 'workbenchManager', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @workbenchRootId, ' '), 30, '', 1, @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex');
SET @imMailboxConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex' LIMIT 1);
UPDATE `hg_admin_menu`
SET `pid`=@workbenchRootId, `path`='mailbox-config/index', `active_menu`='workbenchManager',
- `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=20,
+ `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=30,
`status`=1, `hidden`=0, `updated_at`=@now
WHERE `id`=@imMailboxConfigId;
diff --git a/server/storage/data/generate/im_menu_hierarchy.sql b/server/storage/data/generate/im_menu_hierarchy.sql
index f956eee..287aa3f 100644
--- a/server/storage/data/generate/im_menu_hierarchy.sql
+++ b/server/storage/data/generate/im_menu_hierarchy.sql
@@ -37,7 +37,6 @@ UPDATE `hg_admin_menu` SET `redirect`='/im/dashboard/index', `updated_at`=@now W
UPDATE `hg_admin_menu` SET `pid`=@peopleId, `title`='用户管理', `path`='user/index', `level`=3, `tree`=CONCAT('tr_', @imRootId, ' tr_', @peopleId, ' '), `sort`=10, `updated_at`=@now WHERE `name`='imUserIndex';
UPDATE `hg_admin_menu` SET `pid`=@peopleId, `path`='user-policy-group/index', `level`=3, `tree`=CONCAT('tr_', @imRootId, ' tr_', @peopleId, ' '), `sort`=20, `updated_at`=@now WHERE `name`='imUserPolicyGroupIndex';
UPDATE `hg_admin_menu` SET `pid`=@peopleId, `title`='群组管理', `path`='group/index', `level`=3, `tree`=CONCAT('tr_', @imRootId, ' tr_', @peopleId, ' '), `sort`=30, `updated_at`=@now WHERE `name`='imGroupIndex';
-UPDATE `hg_admin_menu` SET `pid`=@peopleId, `path`='org/index', `level`=3, `tree`=CONCAT('tr_', @imRootId, ' tr_', @peopleId, ' '), `sort`=40, `updated_at`=@now WHERE `name`='imOrgIndex';
UPDATE `hg_admin_menu` SET `pid`=@peopleId, `title`='邀请码管理', `path`='registration-invite/index', `level`=3, `tree`=CONCAT('tr_', @imRootId, ' tr_', @peopleId, ' '), `sort`=50, `updated_at`=@now WHERE `name`='imRegistrationInviteIndex';
-- Content review.
@@ -97,15 +96,17 @@ WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'workbenchManager
SET @workbenchRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'workbenchManager' LIMIT 1);
SET @workbenchId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex' LIMIT 1);
+SET @orgId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imOrgIndex' LIMIT 1);
SET @mailboxConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex' LIMIT 1);
SET @feedbackConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex' LIMIT 1);
UPDATE `hg_admin_menu` SET `pid`=0, `title`='工作台管理', `path`='/workbench', `icon`='AppstoreOutlined', `type`=1, `redirect`='/workbench/config/index', `component`='LAYOUT', `always_show`=1, `active_menu`='', `is_root`=1, `hidden`=0, `level`=1, `tree`=CONCAT('tr_', `id`, ' '), `sort`=126, `status`=1, `updated_at`=@now WHERE `id`=@workbenchRootId;
UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `title`='工作台配置', `path`='config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=10, `status`=1, `hidden`=0, `updated_at`=@now WHERE `id`=@workbenchId;
-UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='mailbox-config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=20, `status`=1, `hidden`=0, `updated_at`=@now WHERE `id`=@mailboxConfigId;
-UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='feedback-config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=30, `status`=1, `hidden`=0, `updated_at`=@now WHERE `id`=@feedbackConfigId;
+UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `title`='组织架构', `path`='org/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=20, `status`=1, `hidden`=0, `updated_at`=@now WHERE `id`=@orgId;
+UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='mailbox-config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=30, `status`=1, `hidden`=0, `updated_at`=@now WHERE `id`=@mailboxConfigId;
+UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='feedback-config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=40, `status`=1, `hidden`=0, `updated_at`=@now WHERE `id`=@feedbackConfigId;
UPDATE `hg_admin_menu` SET `level`=3, `tree`=CONCAT('tr_', @workbenchRootId, ' tr_', @workbenchId, ' '), `updated_at`=@now WHERE `pid`=@workbenchId AND `type`=3;
-UPDATE `hg_admin_menu` child JOIN `hg_admin_menu` page ON page.`id`=child.`pid` SET child.`level`=3, child.`tree`=CONCAT('tr_', @workbenchRootId, ' tr_', page.`id`, ' '), child.`updated_at`=@now WHERE page.`id` IN (@mailboxConfigId, @feedbackConfigId) AND child.`type`=3;
+UPDATE `hg_admin_menu` child JOIN `hg_admin_menu` page ON page.`id`=child.`pid` SET child.`level`=3, child.`tree`=CONCAT('tr_', @workbenchRootId, ' tr_', page.`id`, ' '), child.`updated_at`=@now WHERE page.`id` IN (@orgId, @mailboxConfigId, @feedbackConfigId) AND child.`type`=3;
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
-SELECT `role_id`, @workbenchRootId FROM `hg_admin_role_menu` WHERE `menu_id` IN (@workbenchId, @mailboxConfigId, @feedbackConfigId);
+SELECT `role_id`, @workbenchRootId FROM `hg_admin_role_menu` WHERE `menu_id` IN (@workbenchId, @orgId, @mailboxConfigId, @feedbackConfigId);
diff --git a/server/storage/data/generate/im_menu_title_repair.sql b/server/storage/data/generate/im_menu_title_repair.sql
index da41e76..b37e3e7 100644
--- a/server/storage/data/generate/im_menu_title_repair.sql
+++ b/server/storage/data/generate/im_menu_title_repair.sql
@@ -48,6 +48,7 @@ UPDATE `hg_admin_menu` SET `title` = '创建邀请码' WHERE `name` = 'imInviteC
UPDATE `hg_admin_menu` SET `title` = '批量创建邀请码' WHERE `name` = 'imInviteBatchCreate';
UPDATE `hg_admin_menu` SET `title` = '批次详情' WHERE `name` = 'imInviteBatchView';
UPDATE `hg_admin_menu` SET `title` = '撤销邀请码' WHERE `name` = 'imInviteRevoke';
+UPDATE `hg_admin_menu` SET `title` = '绑定用户' WHERE `name` = 'imInviteBindUser';
UPDATE `hg_admin_menu` SET `title` = '撤销批次邀请码' WHERE `name` = 'imInviteBatchRevoke';
UPDATE `hg_admin_menu` SET `title` = '导出邀请码' WHERE `name` = 'imInviteExport';
diff --git a/server/storage/data/generate/im_missing_menu_patch.sql b/server/storage/data/generate/im_missing_menu_patch.sql
index a991f51..24584fa 100644
--- a/server/storage/data/generate/im_missing_menu_patch.sql
+++ b/server/storage/data/generate/im_missing_menu_patch.sql
@@ -87,9 +87,10 @@ WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name`='workbenchManager')
SET @workbenchRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name`='workbenchManager' LIMIT 1);
UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=10, `updated_at`=@now WHERE `id`=@workbenchId;
-UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='mailbox-config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=20, `updated_at`=@now WHERE `id`=@mailboxConfigId;
-UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='feedback-config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=30, `updated_at`=@now WHERE `id`=@feedbackConfigId;
-UPDATE `hg_admin_menu` child JOIN `hg_admin_menu` page ON page.`id`=child.`pid` SET child.`level`=3, child.`tree`=CONCAT('tr_', @workbenchRootId, ' tr_', page.`id`, ' '), child.`updated_at`=@now WHERE page.`id` IN (@workbenchId, @mailboxConfigId, @feedbackConfigId) AND child.`type`=3;
+UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `title`='组织架构', `path`='org/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=20, `updated_at`=@now WHERE `id`=@imOrgId;
+UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='mailbox-config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=30, `updated_at`=@now WHERE `id`=@mailboxConfigId;
+UPDATE `hg_admin_menu` SET `pid`=@workbenchRootId, `path`='feedback-config/index', `active_menu`='workbenchManager', `level`=2, `tree`=CONCAT('tr_', @workbenchRootId, ' '), `sort`=40, `updated_at`=@now WHERE `id`=@feedbackConfigId;
+UPDATE `hg_admin_menu` child JOIN `hg_admin_menu` page ON page.`id`=child.`pid` SET child.`level`=3, child.`tree`=CONCAT('tr_', @workbenchRootId, ' tr_', page.`id`, ' '), child.`updated_at`=@now WHERE page.`id` IN (@workbenchId, @imOrgId, @mailboxConfigId, @feedbackConfigId) AND child.`type`=3;
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
-SELECT `role_id`, @workbenchRootId FROM `hg_admin_role_menu` WHERE `menu_id` IN (@workbenchId, @mailboxConfigId, @feedbackConfigId);
+SELECT `role_id`, @workbenchRootId FROM `hg_admin_role_menu` WHERE `menu_id` IN (@workbenchId, @imOrgId, @mailboxConfigId, @feedbackConfigId);
diff --git a/server/storage/data/generate/im_org_menu.sql b/server/storage/data/generate/im_org_menu.sql
index e4737d1..34d59d1 100644
--- a/server/storage/data/generate/im_org_menu.sql
+++ b/server/storage/data/generate/im_org_menu.sql
@@ -1,40 +1,60 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
+SET @workbenchRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'workbenchManager' 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, '组织架构', 'imOrgIndex', 'org/index', '', '2', '', '/imOrg/departmentList', '', '/imOrg/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '65', '', '1', @now, @now
+SELECT NULL, COALESCE(@workbenchRootId, @imRootId), '组织架构', 'imOrgIndex', 'org/index', '', '2', '', '/imOrg/departmentList', '', '/imOrg/index', '1', IF(@workbenchRootId IS NULL, 'imManager', 'workbenchManager'), '0', '0', '', '0', '0', '0', IF(@workbenchRootId IS NULL, '2', '2'), CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgIndex');
SET @imOrgId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imOrgIndex' LIMIT 1);
+UPDATE `hg_admin_menu`
+SET `pid` = COALESCE(@workbenchRootId, @imRootId),
+ `title` = '组织架构',
+ `path` = 'org/index',
+ `active_menu` = IF(@workbenchRootId IS NULL, 'imManager', 'workbenchManager'),
+ `level` = 2,
+ `tree` = CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' '),
+ `sort` = 20,
+ `status` = 1,
+ `hidden` = 0,
+ `updated_at` = @now
+WHERE `id` = @imOrgId;
+
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, @imOrgId, '部门编辑', 'imOrgDepartmentEdit', '', '', '3', '', '/imOrg/departmentEdit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '10', '', '1', @now, @now
+SELECT NULL, @imOrgId, '部门编辑', 'imOrgDepartmentEdit', '', '', '3', '', '/imOrg/departmentEdit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgDepartmentEdit');
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, @imOrgId, '部门导出', 'imOrgDepartmentExport', '', '', '3', '', '/imOrg/departmentExport', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '15', '', '1', @now, @now
+SELECT NULL, @imOrgId, '部门导出', 'imOrgDepartmentExport', '', '', '3', '', '/imOrg/departmentExport', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '), '15', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgDepartmentExport');
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, @imOrgId, '部门删除', 'imOrgDepartmentDelete', '', '', '3', '', '/imOrg/departmentDelete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '20', '', '1', @now, @now
+SELECT NULL, @imOrgId, '部门删除', 'imOrgDepartmentDelete', '', '', '3', '', '/imOrg/departmentDelete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgDepartmentDelete');
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, @imOrgId, '成员列表', 'imOrgMemberList', '', '', '3', '', '/imOrg/memberList', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '30', '', '1', @now, @now
+SELECT NULL, @imOrgId, '成员列表', 'imOrgMemberList', '', '', '3', '', '/imOrg/memberList', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgMemberList');
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, @imOrgId, '成员编辑', 'imOrgMemberEdit', '', '', '3', '', '/imOrg/memberEdit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '40', '', '1', @now, @now
+SELECT NULL, @imOrgId, '成员编辑', 'imOrgMemberEdit', '', '', '3', '', '/imOrg/memberEdit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '), '40', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgMemberEdit');
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, @imOrgId, '成员导出', 'imOrgMemberExport', '', '', '3', '', '/imOrg/memberExport', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '45', '', '1', @now, @now
+SELECT NULL, @imOrgId, '成员导出', 'imOrgMemberExport', '', '', '3', '', '/imOrg/memberExport', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '), '45', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgMemberExport');
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, @imOrgId, '成员移除', 'imOrgMemberDelete', '', '', '3', '', '/imOrg/memberDelete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '50', '', '1', @now, @now
+SELECT NULL, @imOrgId, '成员移除', 'imOrgMemberDelete', '', '', '3', '', '/imOrg/memberDelete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '), '50', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgMemberDelete');
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, @imOrgId, '组织搜索', 'imOrgSearch', '', '', '3', '', '/imOrg/search', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '60', '', '1', @now, @now
+SELECT NULL, @imOrgId, '组织搜索', 'imOrgSearch', '', '', '3', '', '/imOrg/search', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '), '60', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgSearch');
+
+UPDATE `hg_admin_menu`
+SET `level` = 3,
+ `tree` = CONCAT('tr_', COALESCE(@workbenchRootId, @imRootId), ' tr_', @imOrgId, ' '),
+ `updated_at` = @now
+WHERE `pid` = @imOrgId AND `type` = 3;
diff --git a/server/storage/data/generate/im_workbench_menu.sql b/server/storage/data/generate/im_workbench_menu.sql
index 805d7d3..7cb95e5 100644
--- a/server/storage/data/generate/im_workbench_menu.sql
+++ b/server/storage/data/generate/im_workbench_menu.sql
@@ -22,6 +22,7 @@ WHERE @workbenchRootId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex');
SET @imWorkbenchId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex' LIMIT 1);
+SET @imOrgId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imOrgIndex' LIMIT 1);
UPDATE `hg_admin_menu`
SET `pid` = @workbenchRootId, `title` = '工作台配置', `path` = 'config/index',
@@ -31,6 +32,14 @@ SET `pid` = @workbenchRootId, `title` = '工作台配置', `path` = 'config/inde
`updated_at` = @now
WHERE `id` = @imWorkbenchId;
+UPDATE `hg_admin_menu`
+SET `pid` = @workbenchRootId, `title` = '组织架构', `path` = 'org/index',
+ `type` = 2, `permissions` = '/imOrg/departmentList', `component` = '/imOrg/index',
+ `always_show` = 1, `active_menu` = 'workbenchManager', `is_root` = 0, `hidden` = 0,
+ `level` = 2, `tree` = CONCAT('tr_', @workbenchRootId, ' '), `sort` = 20, `status` = 1,
+ `updated_at` = @now
+WHERE `id` = @imOrgId;
+
INSERT INTO `hg_admin_menu` (`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 @imWorkbenchId, item.title, item.name, '', '', 3, '', item.permissions, '', '', 1, '', 0, 0, '', 0, 1, 0, 3, CONCAT('tr_', @workbenchRootId, ' tr_', @imWorkbenchId, ' '), item.sort, '', 1, @now, @now
FROM (
@@ -51,13 +60,13 @@ SET @feedbackConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imFee
UPDATE `hg_admin_menu`
SET `pid` = @workbenchRootId, `path` = 'mailbox-config/index', `active_menu` = 'workbenchManager',
- `level` = 2, `tree` = CONCAT('tr_', @workbenchRootId, ' '), `sort` = 20,
+ `level` = 2, `tree` = CONCAT('tr_', @workbenchRootId, ' '), `sort` = 30,
`status` = 1, `hidden` = 0, `updated_at` = @now
WHERE `id` = @mailboxConfigId;
UPDATE `hg_admin_menu`
SET `pid` = @workbenchRootId, `path` = 'feedback-config/index', `active_menu` = 'workbenchManager',
- `level` = 2, `tree` = CONCAT('tr_', @workbenchRootId, ' '), `sort` = 30,
+ `level` = 2, `tree` = CONCAT('tr_', @workbenchRootId, ' '), `sort` = 40,
`status` = 1, `hidden` = 0, `updated_at` = @now
WHERE `id` = @feedbackConfigId;
@@ -66,11 +75,11 @@ JOIN `hg_admin_menu` page ON page.`id` = child.`pid`
SET child.`level` = 3,
child.`tree` = CONCAT('tr_', @workbenchRootId, ' tr_', page.`id`, ' '),
child.`updated_at` = @now
-WHERE page.`id` IN (@mailboxConfigId, @feedbackConfigId)
+WHERE page.`id` IN (@imOrgId, @mailboxConfigId, @feedbackConfigId)
AND child.`type` = 3;
-- Preserve access for every role which could already open a workbench feature.
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
SELECT `role_id`, @workbenchRootId
FROM `hg_admin_role_menu`
-WHERE `menu_id` IN (@imWorkbenchId, @mailboxConfigId, @feedbackConfigId);
+WHERE `menu_id` IN (@imWorkbenchId, @imOrgId, @mailboxConfigId, @feedbackConfigId);
diff --git a/server/storage/data/im_registration_invite_hotgo.sql b/server/storage/data/im_registration_invite_hotgo.sql
index 6301bfc..eed8f80 100644
--- a/server/storage/data/im_registration_invite_hotgo.sql
+++ b/server/storage/data/im_registration_invite_hotgo.sql
@@ -81,6 +81,7 @@ CREATE TABLE IF NOT EXISTS im_registration_invite_batches (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
quantity INT UNSIGNED NOT NULL,
+ max_uses_per_code INT UNSIGNED NOT NULL DEFAULT 0,
idempotency_key VARCHAR(128) NOT NULL,
expires_at DATETIME(3) NOT NULL,
created_by BIGINT NOT NULL,
@@ -99,6 +100,9 @@ CREATE TABLE IF NOT EXISTS im_registration_invite_codes (
code CHAR(5) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
status VARCHAR(16) NOT NULL,
expires_at DATETIME(3) NOT NULL,
+ max_uses INT UNSIGNED NOT NULL DEFAULT 0,
+ bound_user_id BIGINT NULL,
+ bound_user_label VARCHAR(128) NOT NULL DEFAULT '',
usage_count INT UNSIGNED NOT NULL DEFAULT 0,
last_used_at DATETIME(3) NULL,
created_by BIGINT NOT NULL,
@@ -107,6 +111,7 @@ CREATE TABLE IF NOT EXISTS im_registration_invite_codes (
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_im_registration_invite_code_tenant (tenant_id, code),
+ KEY idx_im_registration_invite_tenant_bound_user (tenant_id, bound_user_id),
KEY idx_im_registration_invite_tenant_status_expires (tenant_id, status, expires_at),
KEY idx_im_registration_invite_tenant_batch (tenant_id, batch_id),
KEY idx_im_registration_invite_tenant_created (tenant_id, created_at)
@@ -166,8 +171,9 @@ FROM (
SELECT '批量生成邀请码', 'imInviteBatchCreate', '/imInvite/batchCreate', 20 UNION ALL
SELECT '批次详情', 'imInviteBatchView', '/imInvite/batchView', 30 UNION ALL
SELECT '撤销邀请码', 'imInviteRevoke', '/imInvite/revoke', 40 UNION ALL
- SELECT '批量撤销邀请码', 'imInviteBatchRevoke', '/imInvite/batchRevoke', 50 UNION ALL
- SELECT '导出邀请码', 'imInviteExport', '/imInvite/export', 60
+ SELECT '绑定用户', 'imInviteBindUser', '/imInvite/bindUser', 50 UNION ALL
+ SELECT '批量撤销邀请码', 'imInviteBatchRevoke', '/imInvite/batchRevoke', 60 UNION ALL
+ SELECT '导出邀请码', 'imInviteExport', '/imInvite/export', 70
) p
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu m WHERE m.name = p.name);
diff --git a/web/src/api/im/registrationInvite.ts b/web/src/api/im/registrationInvite.ts
index 3bc8c48..9c192c5 100644
--- a/web/src/api/im/registrationInvite.ts
+++ b/web/src/api/im/registrationInvite.ts
@@ -40,6 +40,14 @@ export function Revoke(params) {
});
}
+export function BindUser(params) {
+ return http.request({
+ url: '/imInvite/bindUser',
+ method: 'POST',
+ params,
+ });
+}
+
export function BatchRevoke(params) {
return http.request({
url: '/imInvite/batchRevoke',
diff --git a/web/src/views/im/registrationInvite/BatchViewModal.vue b/web/src/views/im/registrationInvite/BatchViewModal.vue
index 5fc54e1..4928f0c 100644
--- a/web/src/views/im/registrationInvite/BatchViewModal.vue
+++ b/web/src/views/im/registrationInvite/BatchViewModal.vue
@@ -1,7 +1,6 @@
- {{ detail.batchId || '-' }}
{{ detail.quantity || 0 }}
{{ detail.activeCount || 0 }}
{{ detail.usageCount || 0 }}
@@ -27,6 +26,16 @@
{ title: '邀请码', key: 'code' },
{ title: '状态', key: 'status' },
{ title: '累计使用次数', key: 'usageCount' },
+ {
+ title: '可用次数',
+ key: 'availableUses',
+ render(row: any) {
+ if (!row.maxUses || Number(row.maxUses) <= 0) {
+ return '不限制';
+ }
+ return `${Math.max(Number(row.maxUses) - Number(row.usageCount || 0), 0)}`;
+ },
+ },
{ title: '最后使用时间', key: 'lastUsedAt' },
];
diff --git a/web/src/views/im/registrationInvite/BindUserModal.vue b/web/src/views/im/registrationInvite/BindUserModal.vue
new file mode 100644
index 0000000..7ddac28
--- /dev/null
+++ b/web/src/views/im/registrationInvite/BindUserModal.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+
+ 取消
+
+ 确认绑定
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/im/registrationInvite/index.vue b/web/src/views/im/registrationInvite/index.vue
index f6cf6fc..1f30c87 100644
--- a/web/src/views/im/registrationInvite/index.vue
+++ b/web/src/views/im/registrationInvite/index.vue
@@ -45,6 +45,7 @@
+
@@ -69,6 +70,7 @@
import { adaTableScrollX } from '@/utils/hotgo';
import CreateModal from './CreateModal.vue';
import BatchViewModal from './BatchViewModal.vue';
+ import BindUserModal from './BindUserModal.vue';
const message = useMessage();
const dialog = useDialog();
@@ -78,6 +80,7 @@
const searchFormRef = ref({});
const createRef = ref();
const batchViewRef = ref();
+ const bindUserRef = ref();
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
const tableColumns = computed(() =>
@@ -98,7 +101,7 @@
);
const actionColumn = reactive({
- width: 300,
+ width: 380,
title: '操作',
key: 'action',
fixed: 'right',
@@ -106,6 +109,12 @@
return h(TableAction as any, {
style: 'button',
actions: [
+ {
+ label: '绑定用户',
+ onClick: handleBindUser.bind(null, record),
+ auth: ['/imInvite/bindUser'],
+ ifShow: () => record.status === 'active' && !record.boundUserId && Number(record.usageCount || 0) === 0,
+ },
{
label: '批次',
onClick: handleBatchView.bind(null, record),
@@ -168,6 +177,10 @@
batchViewRef.value.openModal(Number(record.tenantId || selectedTenantId()), record.batchId);
}
+ function handleBindUser(record: State) {
+ bindUserRef.value.openModal(Number(record.tenantId || selectedTenantId()), record.id);
+ }
+
function handleRevoke(record: State) {
dialog.warning({
title: '确认撤销',
@@ -185,7 +198,7 @@
function handleBatchRevoke(record: State) {
dialog.warning({
title: '确认撤销批次',
- content: `将撤销批次 ${record.batchId} 下仍有效的邀请码,已过期或已撤销的邀请码不受影响,是否确认?`,
+ content: '将撤销该批次下仍有效的邀请码,已过期或已撤销的邀请码不受影响,是否确认?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
diff --git a/web/src/views/im/registrationInvite/model.ts b/web/src/views/im/registrationInvite/model.ts
index 26272ae..4e74e04 100644
--- a/web/src/views/im/registrationInvite/model.ts
+++ b/web/src/views/im/registrationInvite/model.ts
@@ -16,7 +16,10 @@ export class State {
public code = '';
public status = '';
public expiresAt = '';
+ public maxUses = 0;
public usageCount = 0;
+ public boundUserId = '';
+ public boundUserLabel = '';
public lastUsedAt = '';
public createdBy = '';
public remark = '';
@@ -59,14 +62,6 @@ export const schemas = ref([
placeholder: '请选择状态',
},
},
- {
- field: 'batchId',
- component: 'NInput',
- label: '批次ID',
- componentProps: {
- placeholder: '请输入批次ID',
- },
- },
{
field: 'createdBy',
component: 'NInput',
@@ -102,9 +97,26 @@ export const columns = [
{ title: '商户', key: 'tenantName', align: 'left', width: 160 },
{ title: '邀请码', key: 'code', align: 'left', width: 120 },
{ title: '状态', key: 'status', align: 'left', width: 110 },
- { title: '批次ID', key: 'batchId', align: 'left', width: 150 },
{ title: '到期时间', key: 'expiresAt', align: 'left', width: 190 },
{ title: '累计使用次数', key: 'usageCount', align: 'left', width: 130 },
+ {
+ title: '可用次数',
+ key: 'availableUses',
+ align: 'left',
+ width: 110,
+ render(row: State) {
+ return inviteAvailableUsesLabel(row);
+ },
+ },
+ {
+ title: '绑定用户',
+ key: 'boundUserLabel',
+ align: 'left',
+ width: 180,
+ render(row: State) {
+ return row.boundUserLabel || row.boundUserId || '--';
+ },
+ },
{ title: '最后使用时间', key: 'lastUsedAt', align: 'left', width: 190 },
{ title: '创建人', key: 'createdBy', align: 'left', width: 110 },
{ title: '创建时间', key: 'createdAt', align: 'left', width: 190 },
@@ -128,6 +140,13 @@ export function statusLabel(status: string) {
return statusOptions.find((item) => item.value === status)?.label || status;
}
+export function inviteAvailableUsesLabel(row: State) {
+ if (!row.maxUses || Number(row.maxUses) <= 0) {
+ return '不限制';
+ }
+ return `${Math.max(Number(row.maxUses) - Number(row.usageCount || 0), 0)}`;
+}
+
export function localRfc3339(value: number | null) {
if (!value) {
return '';