{"record":{"id":"da642d060740ab0f","repo":"multica-ai/multica","slug":"wecom-user-id-is-already-bound-to-a-different-use","errorCode":null,"errorMessage":"wecom: user id is already bound to a different user","messagePattern":"wecom: user id is already bound to a different user","errorType":"exception","errorClass":null,"httpStatus":409,"severity":"error","filePath":"server/internal/integrations/wecom/binding.go","lineNumber":75,"sourceCode":"//\n// Sixty seconds does the job the throttle was written for: six lines typed in\n// one breath still write one row, at a cost of at most one row a minute for a\n// user who keeps going, against rows that expire in fifteen. The price of\n// being wrong is one more message, not ten minutes of a bot insisting it\n// already answered.\n//\n// It must stay comfortably inside BindingTokenTTL so a link a throttled user\n// is pointed back at still has real time left on it.\nconst BindingTokenMintInterval = time.Minute\n\nvar (\n\t// ErrBindingTokenInvalid: token unknown / already consumed / expired.\n\t// One opaque error for all three avoids a replay timing oracle.\n\tErrBindingTokenInvalid = errors.New(\"wecom: binding token invalid or expired\")\n\t// ErrBindingAlreadyAssigned: this WeCom userid is already bound to a\n\t// different Multica user (account transfer must go through explicit\n\t// unbind, not implemented in iter 1 — an admin can DELETE the row).\n\tErrBindingAlreadyAssigned = errors.New(\"wecom: user id is already bound to a different user\")\n\t// ErrBindingNotWorkspaceMember: the redeemer is not a member of the\n\t// token's workspace. Translated to 403 at the HTTP boundary.\n\tErrBindingNotWorkspaceMember = errors.New(\"wecom: redeemer is not a workspace member\")\n)\n\n// BindingToken is a freshly minted token. The raw value is returned exactly\n// once (embedded in the binding URL); only its hash is persisted.\ntype BindingToken struct {\n\tRaw       string\n\tExpiresAt time.Time\n\n\t// Reused says the throttle suppressed the mint because a live link is\n\t// already sitting in the user's chat. Raw is empty in that case and there\n\t// is no way to recover it — the table only ever held the hash — so the\n\t// caller must point the user back at the earlier message rather than\n\t// building a URL. ExpiresAt carries the live token's expiry, not a fresh\n\t// one's.\n\tReused bool","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/integrations/wecom/binding.go#L57-L93","documentation":"WeCom binding sentinel error: this WeCom userid is already bound to a different Multica user. Account transfer requires an explicit unbind, which is NOT implemented in iteration 1 — the documented remedy is for an admin to DELETE the binding row directly.","triggerScenarios":"Redeeming a WeCom binding token while an existing channel user-binding maps the same WeCom userid to another Multica user — e.g. the person previously linked their WeCom to a first account and now redeems a link while signed in as a second.","commonSituations":"Account migration (employee got a new Multica account, WeCom identity unchanged); shared WeCom bot across teams where a colleague already bound the id; test environments reusing one WeCom userid.","solutions":["Have an admin delete the existing wecom/channel user-binding row for that userid (the iteration-1 stand-in for unbind), then request a fresh binding link.","Alternatively, sign in as the Multica account that already owns the binding and simply use it.","Track the unbind flow's implementation status before promising self-service transfer in the UI."],"exampleFix":"// before\n_, err := svc.Redeem(ctx, rawToken)\n// -> \"wecom: user id is already bound to a different user\"\n\n// after: iteration-1 remedy — admin removes the row, user re-links\nif errors.Is(err, wecom.ErrBindingAlreadyAssigned) {\n\trespond(w, 409, \"already bound to another account — an admin must remove the binding before re-linking\")\n\treturn\n}","handlingStrategy":"try-catch","validationCode":"owner, err := store.GetChannelUserBinding(ctx, \"wecom\", wecomUserID)\nif err == nil && owner != redeemingUserID {\n\treturn respondConflict(w, \"already bound — admin must remove the binding row (unbind flow not implemented in iter 1)\")\n}\n_ = wecomSvc.Redeem(ctx, rawToken)","typeGuard":null,"tryCatchPattern":"_, err := wecomSvc.Redeem(ctx, rawToken)\nif err != nil {\n\tif errors.Is(err, wecom.ErrBindingAlreadyAssigned) {\n\t\treturn respondConflict(w, \"bound to another account — an admin must DELETE the binding row\")\n\t}\n\treturn err\n}","preventionTips":["Keep WeCom userids one-to-one with Multica accounts in every environment.","Until the unbind flow ships, give admins a tool/SQL path to remove binding rows on request.","Test account migrations with a fresh WeCom userid rather than rebinding an owned one."],"tags":["wecom","binding","account-linking","integration","go"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}