import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; import { nowIso } from "./_utils/time"; import type { Id } from "./_generated/dataModel"; async function requireUserId(ctx: any): Promise { const userId = await getAuthUserId(ctx); if (userId === null) throw new Error("未登录"); return String(userId); } async function requireWorkspaceMember(ctx: any, workspaceId: string, userId: string) { const member = await ctx.db .query("workspace_members") .withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", workspaceId).eq("user_id", userId)) .first(); if (!member) throw new Error("无权限"); return member; } async function requireGroup(ctx: any, groupId: string) { const group = await ctx.db .query("groups") .withIndex("by_group_id", (q: any) => q.eq("id", groupId)) .first(); if (!group) throw new Error("群组不存在"); return group; } async function requireGroupOwner(ctx: any, groupId: string, userId: string) { const membership = await ctx.db .query("group_members") .withIndex("by_group_user", (q: any) => q.eq("group_id", groupId).eq("user_id", userId)) .first(); if (!membership || membership.role !== "owner") throw new Error("无权限"); return membership; } function normalizeUsername(raw: string): string { const username = String(raw ?? "").trim(); if (!username) throw new Error("用户名不能为空"); if (username.length < 2 || username.length > 32) throw new Error("用户名长度需为 2-32 个字符"); if (/\s/.test(username)) throw new Error("用户名不能包含空格"); return username; } async function lookupUserIdByUsername(ctx: any, rawUsername: string): Promise { const username = normalizeUsername(rawUsername); const user = await ctx.db .query("users") .filter((q: any) => q.eq(q.field("name"), username)) .first(); if (!user) throw new Error("未找到该用户"); return String(user._id); } export const listMineByWorkspace = query({ args: { workspaceId: v.string() }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); const invites = await ctx.db .query("group_invitations") .withIndex("by_workspace_invited_user", (q: any) => q.eq("workspace_id", args.workspaceId).eq("invited_user_id", userId), ) .collect(); // 最新的排前面 invites.sort((a: any, b: any) => String(b.updated_at ?? "").localeCompare(String(a.updated_at ?? ""))); const result = []; for (const inv of invites) { const group = await ctx.db .query("groups") .withIndex("by_group_id", (q: any) => q.eq("id", inv.group_id)) .first(); const inviter = await ctx.db.get(inv.invited_by as Id<"users">); result.push({ groupId: inv.group_id, groupName: group?.name ?? null, invitedByUserId: inv.invited_by, invitedByUsername: inviter?.name ?? null, createdAt: inv.created_at, updatedAt: inv.updated_at, }); } return result; }, }); export const listMine = query({ args: {}, handler: async (ctx) => { const userId = await requireUserId(ctx); const invites = await ctx.db .query("group_invitations") .withIndex("by_invited_user", (q: any) => q.eq("invited_user_id", userId)) .collect(); invites.sort((a: any, b: any) => String(b.updated_at ?? "").localeCompare(String(a.updated_at ?? ""))); const result = []; for (const inv of invites) { const group = await ctx.db .query("groups") .withIndex("by_group_id", (q: any) => q.eq("id", inv.group_id)) .first(); const workspace = await ctx.db .query("workspaces") .withIndex("by_workspace_id", (q: any) => q.eq("id", inv.workspace_id)) .first(); const inviter = await ctx.db.get(inv.invited_by as Id<"users">); result.push({ workspaceId: inv.workspace_id, workspaceName: workspace?.name ?? null, groupId: inv.group_id, groupName: group?.name ?? null, invitedByUserId: inv.invited_by, invitedByUsername: inviter?.name ?? null, createdAt: inv.created_at, updatedAt: inv.updated_at, }); } return result; }, }); export const inviteByUsername = mutation({ args: { groupId: v.string(), username: v.string() }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); const group = await requireGroup(ctx, args.groupId); await requireWorkspaceMember(ctx, group.workspace_id, userId); await requireGroupOwner(ctx, args.groupId, userId); const targetUserId = await lookupUserIdByUsername(ctx, args.username); if (targetUserId === userId) throw new Error("不能邀请自己"); const existingMember = await ctx.db .query("group_members") .withIndex("by_group_user", (q: any) => q.eq("group_id", args.groupId).eq("user_id", targetUserId)) .first(); if (existingMember) { return { ok: true }; } const ts = nowIso(); const existingInvite = await ctx.db .query("group_invitations") .withIndex("by_group_user", (q: any) => q.eq("group_id", args.groupId).eq("invited_user_id", targetUserId)) .first(); if (existingInvite) { await ctx.db.patch(existingInvite._id, { invited_by: userId, updated_at: ts }); return { ok: true }; } await ctx.db.insert("group_invitations", { workspace_id: group.workspace_id, group_id: args.groupId, invited_user_id: targetUserId, invited_by: userId, created_at: ts, updated_at: ts, }); return { ok: true }; }, }); export const accept = mutation({ args: { groupId: v.string() }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); const group = await requireGroup(ctx, args.groupId); await requireWorkspaceMember(ctx, group.workspace_id, userId).catch(async () => { // 说明:允许被邀请者尚未加入 workspace,接受邀请时自动加入。 await ctx.db.insert("workspace_members", { workspace_id: group.workspace_id, user_id: userId, role: "member", is_default: false, created_at: nowIso(), }); }); const invite = await ctx.db .query("group_invitations") .withIndex("by_group_user", (q: any) => q.eq("group_id", args.groupId).eq("invited_user_id", userId)) .first(); if (!invite) { return { ok: true }; } const existingMember = await ctx.db .query("group_members") .withIndex("by_group_user", (q: any) => q.eq("group_id", args.groupId).eq("user_id", userId)) .first(); const ts = nowIso(); if (!existingMember) { await ctx.db.insert("group_members", { workspace_id: group.workspace_id, group_id: args.groupId, user_id: userId, role: "member", created_at: ts, }); } await ctx.db.delete(invite._id); return { ok: true }; }, }); export const decline = mutation({ args: { groupId: v.string() }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); const group = await requireGroup(ctx, args.groupId); await requireWorkspaceMember(ctx, group.workspace_id, userId).catch(() => { // 即使没进 workspace,也可以拒绝邀请(删除邀请记录)。 }); const invite = await ctx.db .query("group_invitations") .withIndex("by_group_user", (q: any) => q.eq("group_id", args.groupId).eq("invited_user_id", userId)) .first(); if (invite) { await ctx.db.delete(invite._id); } return { ok: true }; }, });