// VL_VERSION:4.2.13
<ServiceDomain-UserCenterService>

# Backend Tree
<VirtualTable-AuthUsers "authUsers"> sourceTable:AuthUsers
-<Field-nickname> type:STRING
-<Field-display_name> type:STRING
-<Field-avatar> type:STRING
-<Field-status> type:STRING
-<Field-primary_email> type:STRING
-<Field-primary_phone> type:STRING
-<Field-last_login_at> type:TIMESTAMP
-<Field-extra_json> type:JSON
<VirtualTable-UserCenterConfig "config"> sourceTable:UserCenterConfig
-<Field-nid> type:STRING
-<Field-env> type:STRING
-<Field-displayName> type:STRING
-<Field-logo> type:STRING
-<Field-themeCode> type:STRING
-<Field-registrationOpen> type:BOOL
-<Field-requireApproval> type:BOOL
-<Field-defaultRoleCode> type:STRING
-<Field-passwordPolicyJson> type:JSON
-<Field-sessionDuration> type:INT
-<Field-maxConcurrent> type:INT
-<Field-status> type:STRING
-<Field-permissionCenterBackendNid> type:INT
-<Field-permissionCenterUrl> type:STRING
-<Field-userCenterUrl> type:STRING
<VirtualTable-UserRoles "roles"> sourceTable:UserRoles
-<Field-roleCode> type:STRING
-<Field-name> type:STRING
-<Field-description> type:STRING
-<Field-status> type:STRING
-<Field-sort> type:INT
<VirtualTable-UserRoleAssignments "roleAssign"> sourceTable:UserRoleAssignments
-<Field-userId> type:STRING
-<Field-roleCode> type:STRING
-<Field-assignedAt> type:TIMESTAMP
-<Field-assignedBy> type:STRING
-<Field-status> type:STRING
<VirtualTable-Departments "depts"> sourceTable:Departments
-<Field-deptCode> type:STRING
-<Field-parentDeptCode> type:STRING
-<Field-name> type:STRING
-<Field-status> type:STRING
-<Field-sort> type:INT
<VirtualTable-UserDepartments "userDepts"> sourceTable:UserDepartments
-<Field-userId> type:STRING
-<Field-deptCode> type:STRING
-<Field-isPrimary> type:BOOL
-<Field-assignedAt> type:TIMESTAMP
-<Field-assignedBy> type:STRING
-<Field-status> type:STRING
<VirtualTable-UserCenterRegistry "ucRegistry"> sourceTable:UserCenterRegistry
-<Field-userCenterCode> type:STRING
-<Field-displayName> type:STRING
-<Field-userCenterBackendNid> type:INT
-<Field-userCenterFrontendUrl> type:STRING
-<Field-permissionCenterBackendNid> type:INT
-<Field-permissionCenterFrontendUrl> type:STRING
-<Field-isDefault> type:BOOL
-<Field-status> type:STRING
-<Field-sort> type:INT
<VirtualTable-ProjectUserCenterBindings "projectBindings"> sourceTable:ProjectUserCenterBindings
-<Field-projectId> type:STRING
-<Field-env> type:STRING
-<Field-userCenterCode> type:STRING
-<Field-status> type:STRING
-<Field-boundBy> type:STRING
-<Field-note> type:STRING
<VirtualTable-ProjectBindingChanges "bindingChanges"> sourceTable:ProjectBindingChanges
-<Field-projectId> type:STRING
-<Field-env> type:STRING
-<Field-action> type:STRING
-<Field-fromCode> type:STRING
-<Field-toCode> type:STRING
-<Field-operatorId> type:STRING
-<Field-note> type:STRING
// 受控视图：只声明公开字段，物理上排除 clientId / clientSecretEncrypted / scope / callbackUrl
<VirtualTable-OAuthProviders "oauthProviders"> sourceTable:OAuthProviders
-<Field-providerCode> type:STRING
-<Field-displayName> type:STRING
-<Field-buttonText> type:STRING
-<Field-loginUrl> type:STRING
-<Field-enabled> type:BOOL
// UC-008A 管理视图：含配置字段与 clientSecretEncrypted（仅供写入；List 服务严禁把 secret 放进返回对象）
<VirtualTable-OAuthProvidersAdmin "oauthAdmin"> sourceTable:OAuthProviders
-<Field-providerCode> type:STRING
-<Field-enabled> type:BOOL
-<Field-displayName> type:STRING
-<Field-buttonText> type:STRING
-<Field-loginUrl> type:STRING
-<Field-clientId> type:STRING
-<Field-clientSecretEncrypted> type:STRING
-<Field-scope> type:STRING
-<Field-callbackUrl> type:STRING
<VirtualTable-LoginAuditLogs "audit"> sourceTable:LoginAuditLogs
-<Field-userId> type:STRING
-<Field-accountInput> type:STRING
-<Field-providerCode> type:STRING
-<Field-eventType> type:STRING
-<Field-result> type:STRING
-<Field-failureReason> type:STRING
-<Field-ip> type:STRING
-<Field-userAgent> type:STRING
<UserStore "userStore"> sourceTable:AuthUsers identitiesTable:AuthUserIdentities
<TokenIssuer "tokenIssuer">
<BackendSecurityToolkit "securityToolkit">

# Services

// @meta service description:"内部：幂等的 UserCenterConfig 设置项初始化/修复入口。仅供用户中心自身管理 UI 调用，不暴露为 HTTP。v0.2 移除 nid 输入：身份域 nid 已改为从 SYSENV.currentApp.nid 读取，不再由配置行承载。Ensure 仅维护 displayName / defaults / status / sessionDuration / maxConcurrent。表 0 行 insert，1+ 行 update target + disable 多余。"
SERVICE EnsureUserCenterConfig(displayName(STRING),defaultRoleCode(STRING),sessionDuration(INT),maxConcurrent(INT),permissionCenterBackendNid(INT),permissionCenterUrl(STRING));RETURN {success:BOOL,message:STRING,status:STRING,disabledRowCount:INT,rowsBefore:INT,targetRowId:INT,targetAction:STRING,postDisplayName:STRING,postStatus:STRING,postPermissionCenterBackendNid:INT,postPermissionCenterUrl:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,status:"",disabledRowCount:0,rowsBefore:0,targetRowId:0,targetAction:"denied",postDisplayName:"",postStatus:"",postPermissionCenterBackendNid:0,postPermissionCenterUrl:""}
-ensureUserCenterConfig(displayName, defaultRoleCode, sessionDuration, maxConcurrent, permissionCenterBackendNid, permissionCenterUrl) -> _r
-RETURN _r

// @meta service description:"内部：默认角色集初始化。幂等，已存在不重复创建。仅供用户中心自身管理 UI 调用，不暴露为 HTTP。"
SERVICE SeedDefaultRoles();RETURN {success:BOOL,message:STRING,created:[STRING],existing:[STRING]}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,created:[],existing:[]}
-seedDefaultRoles() -> _r
-RETURN _r

// ── UC-007A：多用户中心注册表与项目绑定管理（仅供用户中心管理 UI 调用，不暴露为 HTTP）────

// @meta service description:"内部：幂等注册当前部署实例为默认用户中心（code=default）。已存在则按当前实例值刷新地址，不改 isDefault/status。UC-008A：需 admin 角色。"
SERVICE EnsureDefaultUserCenter();RETURN {success:BOOL,message:STRING,action:STRING,userCenterCode:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,action:"denied",userCenterCode:""}
-_selfNid(INT) = SYSENV.currentApp.nid
-IF _selfNid == 0
--RETURN {success:false,message:"SYSENV.currentApp.nid unavailable",action:"",userCenterCode:""}
-resolveFrontendUrls() -> _urls
-<VirtualTable-UserCenterRegistry "ucRegistry">.select([["userCenterCode","eq","default"]],[["_id","asc"]],[0,1],null) -> _exist
-IF _exist.dataArray.length == 0
--<VirtualTable-UserCenterRegistry "ucRegistry">.insert({userCenterCode:"default",displayName:"默认用户中心",userCenterBackendNid:_selfNid,userCenterFrontendUrl:_urls.userCenterUrl,permissionCenterBackendNid:_urls.permissionCenterBackendNid,permissionCenterFrontendUrl:_urls.permissionCenterUrl,isDefault:true,status:"active",sort:0}) -> _ins
--IF !_ins.success
---RETURN {success:false,message:_ins.message,action:"insert_failed",userCenterCode:"default"}
--RETURN {success:true,message:"inserted",action:"insert",userCenterCode:"default"}
-_rowId(INT) = _exist.dataArray[0]._id
-<VirtualTable-UserCenterRegistry "ucRegistry">.update([["_id","eq",_rowId]],[["userCenterBackendNid","set",_selfNid],["userCenterFrontendUrl","set",_urls.userCenterUrl],["permissionCenterBackendNid","set",_urls.permissionCenterBackendNid],["permissionCenterFrontendUrl","set",_urls.permissionCenterUrl]],1) -> _upd
-IF !_upd.success
--RETURN {success:false,message:_upd.message,action:"update_failed",userCenterCode:"default"}
-RETURN {success:true,message:"refreshed",action:"update",userCenterCode:"default"}

// @meta service description:"内部：新建/更新任意用户中心实例（多实例管理）。userCenterCode 唯一定位；upsert 同时恢复 active（停用实例可经此重新启用）；不改 isDefault（默认切换走 SetDefaultUserCenter）。UC-008A：需 admin 角色。"
SERVICE UpsertUserCenter(userCenterCode(STRING),displayName(STRING),userCenterBackendNid(INT),userCenterFrontendUrl(STRING),permissionCenterBackendNid(INT),permissionCenterFrontendUrl(STRING));RETURN {success:BOOL,message:STRING,action:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,action:"denied"}
-IF userCenterCode == "" || userCenterCode == NULL
--RETURN {success:false,message:"userCenterCode required",action:""}
-IF userCenterBackendNid <= 0
--RETURN {success:false,message:"userCenterBackendNid required",action:""}
-<VirtualTable-UserCenterRegistry "ucRegistry">.select([["userCenterCode","eq",userCenterCode]],[["_id","asc"]],[0,1],null) -> _exist
-IF _exist.dataArray.length == 0
--<VirtualTable-UserCenterRegistry "ucRegistry">.insert({userCenterCode:userCenterCode,displayName:(displayName ? displayName : ""),userCenterBackendNid:userCenterBackendNid,userCenterFrontendUrl:(userCenterFrontendUrl ? userCenterFrontendUrl : ""),permissionCenterBackendNid:permissionCenterBackendNid,permissionCenterFrontendUrl:(permissionCenterFrontendUrl ? permissionCenterFrontendUrl : ""),isDefault:false,status:"active",sort:0}) -> _ins
--IF !_ins.success
---RETURN {success:false,message:_ins.message,action:"insert_failed"}
--RETURN {success:true,message:"",action:"insert"}
-_rowId(INT) = _exist.dataArray[0]._id
-<VirtualTable-UserCenterRegistry "ucRegistry">.update([["_id","eq",_rowId]],[["displayName","set",(displayName ? displayName : "")],["userCenterBackendNid","set",userCenterBackendNid],["userCenterFrontendUrl","set",(userCenterFrontendUrl ? userCenterFrontendUrl : "")],["permissionCenterBackendNid","set",permissionCenterBackendNid],["permissionCenterFrontendUrl","set",(permissionCenterFrontendUrl ? permissionCenterFrontendUrl : "")],["status","set","active"]],1) -> _upd
-IF !_upd.success
--RETURN {success:false,message:_upd.message,action:"update_failed"}
-RETURN {success:true,message:"",action:"update"}

// @meta service description:"内部：切换默认用户中心。目标实例须 active；先清旧默认标志再设新默认（期间解析兜底落 current 实例合成，不破链）。UC-008A：需 admin 角色。"
SERVICE SetDefaultUserCenter(userCenterCode(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-<VirtualTable-UserCenterRegistry "ucRegistry">.select([["userCenterCode","eq",userCenterCode],["status","eq","active"]],[["_id","asc"]],[0,1],null) -> _target
-IF _target.dataArray.length == 0
--RETURN {success:false,message:("userCenterCode '" + userCenterCode + "' not found or not active")}
-<VirtualTable-UserCenterRegistry "ucRegistry">.update([["isDefault","eq",true]],[["isDefault","set",false]],100) -> _clear
-<VirtualTable-UserCenterRegistry "ucRegistry">.update([["_id","eq",_target.dataArray[0]._id]],[["isDefault","set",true]],1) -> _set
-IF !_set.success || _set.affect == 0
--RETURN {success:false,message:(_set.message ? _set.message : "set default affected 0 rows")}
-RETURN {success:true,message:""}

// @meta service description:"内部：停用用户中心实例。默认实例与仍被 active 项目绑定引用的实例不可停用；恢复经 UpsertUserCenter（upsert 即重新 active）。UC-008A：需 admin 角色。"
SERVICE DisableUserCenter(userCenterCode(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-<VirtualTable-UserCenterRegistry "ucRegistry">.select([["userCenterCode","eq",userCenterCode]],[["_id","asc"]],[0,1],null) -> _target
-IF _target.dataArray.length == 0
--RETURN {success:false,message:"instance not found"}
-IF _target.dataArray[0].isDefault == true
--RETURN {success:false,message:"cannot disable the default user center; switch default first"}
-<VirtualTable-ProjectUserCenterBindings "projectBindings">.count([["userCenterCode","eq",userCenterCode],["status","eq","active"]]) -> _refCnt
-IF _refCnt.count > 0
--RETURN {success:false,message:("instance is referenced by " + _refCnt.count + " active project binding(s); unbind first")}
-<VirtualTable-UserCenterRegistry "ucRegistry">.update([["_id","eq",_target.dataArray[0]._id]],[["status","set","disabled"]],1) -> _upd
-IF !_upd.success
--RETURN {success:false,message:_upd.message}
-RETURN {success:true,message:""}

// @meta service description:"内部：用户中心实例列表（管理 UI）。"
SERVICE ListUserCenters();RETURN {success:BOOL,centers:[{}]}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,centers:[]}
-<VirtualTable-UserCenterRegistry "ucRegistry">.select(null,[["sort","asc"],["_id","asc"]],[0,100],null) -> _r
-RETURN {success:true,centers:_r.dataArray}

// @meta service description:"内部：项目绑定列表（管理 UI）。"
SERVICE ListProjectBindings();RETURN {success:BOOL,bindings:[{}]}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,bindings:[]}
-<VirtualTable-ProjectUserCenterBindings "projectBindings">.select(null,[["_id","desc"]],[0,200],null) -> _r
-RETURN {success:true,bindings:_r.dataArray}

// @meta service description:"内部：新建或更新项目->用户中心中心化绑定（管理 UI 是换绑唯一入口）。projectId+env 唯一：已有行则改 userCenterCode 并置 active。UC-008A：需 admin 角色，写换绑历史。"
SERVICE UpsertProjectBinding(projectId(STRING),env(STRING),userCenterCode(STRING),note(STRING));RETURN {success:BOOL,message:STRING,action:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,action:"denied"}
-IF projectId == "" || projectId == NULL || userCenterCode == "" || userCenterCode == NULL
--RETURN {success:false,message:"projectId and userCenterCode required",action:""}
-_envValue(STRING) = (env != "" && env != NULL) ? env : "preview"
-<VirtualTable-UserCenterRegistry "ucRegistry">.count([["userCenterCode","eq",userCenterCode],["status","eq","active"]]) -> _regCnt
-IF _regCnt.count == 0
--RETURN {success:false,message:("userCenterCode '" + userCenterCode + "' not found or not active in registry"),action:""}
-_operator(STRING) = _guard.userId
-<VirtualTable-ProjectUserCenterBindings "projectBindings">.select([["projectId","eq",projectId],["env","eq",_envValue]],[["_id","asc"]],[0,1],null) -> _exist
-IF _exist.dataArray.length == 0
--<VirtualTable-ProjectUserCenterBindings "projectBindings">.insert({projectId:projectId,env:_envValue,userCenterCode:userCenterCode,status:"active",boundBy:_operator,note:(note ? note : "")}) -> _ins
--IF !_ins.success
---RETURN {success:false,message:_ins.message,action:"insert_failed"}
--<VirtualTable-ProjectBindingChanges "bindingChanges">.insert({projectId:projectId,env:_envValue,action:"bind",fromCode:"",toCode:userCenterCode,operatorId:_operator,note:(note ? note : "")}) -> _h1
--RETURN {success:true,message:"",action:"insert"}
-_rowId(INT) = _exist.dataArray[0]._id
-_fromCode(STRING) = "" + _exist.dataArray[0].userCenterCode
-<VirtualTable-ProjectUserCenterBindings "projectBindings">.update([["_id","eq",_rowId]],[["userCenterCode","set",userCenterCode],["status","set","active"],["boundBy","set",_operator],["note","set",(note ? note : "")]],1) -> _upd
-IF !_upd.success
--RETURN {success:false,message:_upd.message,action:"update_failed"}
-<VirtualTable-ProjectBindingChanges "bindingChanges">.insert({projectId:projectId,env:_envValue,action:"rebind",fromCode:_fromCode,toCode:userCenterCode,operatorId:_operator,note:(note ? note : "")}) -> _h2
-RETURN {success:true,message:"",action:"update"}

// @meta service description:"内部：停用项目绑定（解绑）。解绑后该项目回落到 projectConfig / 默认用户中心。UC-008A：需 admin 角色，写换绑历史。"
SERVICE DisableProjectBinding(projectId(STRING),env(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-_envValue(STRING) = (env != "" && env != NULL) ? env : "preview"
-<VirtualTable-ProjectUserCenterBindings "projectBindings">.select([["projectId","eq",projectId],["env","eq",_envValue]],[["_id","asc"]],[0,1],null) -> _exist
-IF _exist.dataArray.length == 0
--RETURN {success:false,message:"binding not found"}
-_fromCode(STRING) = "" + _exist.dataArray[0].userCenterCode
-<VirtualTable-ProjectUserCenterBindings "projectBindings">.update([["projectId","eq",projectId],["env","eq",_envValue]],[["status","set","disabled"]],1) -> _upd
-IF !_upd.success
--RETURN {success:false,message:_upd.message}
-IF _upd.affect == 0
--RETURN {success:false,message:"binding not found"}
-<VirtualTable-ProjectBindingChanges "bindingChanges">.insert({projectId:projectId,env:_envValue,action:"unbind",fromCode:_fromCode,toCode:"",operatorId:_guard.userId,note:""}) -> _h
-RETURN {success:true,message:""}

// @meta service description:"内部：换绑历史（append-only），管理 UI 展示，可按 projectId 过滤。"
SERVICE ListBindingHistory(projectId(STRING),limit(INT));RETURN {success:BOOL,changes:[{}]}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,changes:[]}
-_safe(INT) = (limit > 0 && limit <= 200) ? limit : 50
-IF projectId != "" && projectId != NULL
--<VirtualTable-ProjectBindingChanges "bindingChanges">.select([["projectId","eq",projectId]],[["_id","desc"]],[0,_safe],null) -> _rf
--RETURN {success:true,changes:_rf.dataArray}
-<VirtualTable-ProjectBindingChanges "bindingChanges">.select(null,[["_id","desc"]],[0,_safe],null) -> _r
-RETURN {success:true,changes:_r.dataArray}

// ── UC-008A：登录配置（缺口2）────────────────────────────────────────────────────────────

// @meta service description:"内部：读取当前登录配置（口径与 resolveUserCenterNid 完全一致，管理页编辑表单回显用）。"
SERVICE GetLoginConfig();RETURN {success:BOOL,message:STRING,displayName:STRING,registrationOpen:BOOL,requireApproval:BOOL,passwordLoginOpen:BOOL,defaultRoleCode:STRING,sessionDuration:INT,maxConcurrent:INT}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,displayName:"",registrationOpen:false,requireApproval:false,passwordLoginOpen:false,defaultRoleCode:"",sessionDuration:0,maxConcurrent:0}
-resolveUserCenterNid() -> _uc
-RETURN {success:_uc.success,message:_uc.message,displayName:_uc.displayName,registrationOpen:_uc.registrationOpen,requireApproval:_uc.requireApproval,passwordLoginOpen:_uc.passwordLoginOpen,defaultRoleCode:_uc.defaultRoleCode,sessionDuration:_uc.sessionDuration,maxConcurrent:_uc.maxConcurrent}

// @meta service description:"内部：保存登录配置（注册开关/审批开关/账号密码登录开关/默认角色/会话时长/最大并发/显示名）。页面先 GetLoginConfig 回显再整体保存。写入后 resolveUserCenterNid/GetClientConfig/注册/登录立即同口径生效。passwordLoginOpen 存 passwordPolicyJson。UC-008A：需 admin 角色。"
SERVICE SaveLoginConfig(displayName(STRING),registrationOpen(BOOL),requireApproval(BOOL),passwordLoginOpen(BOOL),defaultRoleCode(STRING),sessionDuration(INT),maxConcurrent(INT));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-IF defaultRoleCode != "" && defaultRoleCode != NULL
--<VirtualTable-UserRoles "roles">.count([["roleCode","eq",defaultRoleCode],["status","eq","active"]]) -> _roleCnt
--IF _roleCnt.count == 0
---RETURN {success:false,message:("defaultRoleCode '" + defaultRoleCode + "' not found or not active")}
-_sess(INT) = sessionDuration > 0 ? sessionDuration : 7200
-_conc(INT) = maxConcurrent > 0 ? maxConcurrent : 5
-_pwdPolicy({}) = {passwordLoginOpen:passwordLoginOpen}
-<VirtualTable-UserCenterConfig "config">.select(null,[["_id","asc"]],[0,1],null) -> _cfgResult
-IF _cfgResult.dataArray.length == 0
--<VirtualTable-UserCenterConfig "config">.insert({env:"preview",displayName:(displayName ? displayName : ""),defaultRoleCode:(defaultRoleCode ? defaultRoleCode : ""),registrationOpen:registrationOpen,requireApproval:requireApproval,sessionDuration:_sess,maxConcurrent:_conc,status:"active",passwordPolicyJson:_pwdPolicy}) -> _ins
--RETURN {success:_ins.success,message:("" + _ins.message)}
-_rowId(INT) = _cfgResult.dataArray[0]._id
-<VirtualTable-UserCenterConfig "config">.update([["_id","eq",_rowId]],[["displayName","set",(displayName ? displayName : "")],["registrationOpen","set",registrationOpen],["requireApproval","set",requireApproval],["defaultRoleCode","set",(defaultRoleCode ? defaultRoleCode : "")],["sessionDuration","set",_sess],["maxConcurrent","set",_conc],["passwordPolicyJson","set",_pwdPolicy]],1) -> _upd
-IF !_upd.success
--RETURN {success:false,message:_upd.message}
-IF _upd.affect == 0
--RETURN {success:false,message:"config row vanished; retry"}
-RETURN {success:true,message:""}

// ── UC-008A：部门 CSV 批量导入（缺口1）──────────────────────────────────────────────────
// CSV 行格式：deptCode,name,parentDeptCode,sort,status（后三列可省略；status 仅允许 active/disabled，缺省 active）。
// 首行若以 deptCode 开头视为表头跳过。校验与手动 CreateDepartment 同口径：编码/名称必填、编码不得重复
// （CSV 内 + 既有目录）、父节点必须存在（既有目录或本次 CSV 内）、不得自指。CSV 内父子顺序不限：
// 用多趟依赖插入（父先于子），无法落位的行（未知父/环路）整行报错跳过，不阻断其余行。
// @meta service description:"内部：部门 CSV 批量导入。返回逐行错误明细与统计；valid 行全部入库，错误行跳过。UC-008A：需 admin 角色。"
SERVICE ImportDepartments(csvText(STRING));RETURN {success:BOOL,message:STRING,total:INT,created:INT,failed:INT,errors:[{}]}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,total:0,created:0,failed:0,errors:[]}
-IF csvText == "" || csvText == NULL
--RETURN {success:false,message:"csvText is empty",total:0,created:0,failed:0,errors:[]}
-_lines([STRING]) = csvText.replaceAll("\r", "").split("\n")
-_codes([STRING]) = []
-_names([STRING]) = []
-_parents([STRING]) = []
-_sorts([INT]) = []
-_statuses([STRING]) = []
-_lineNos([INT]) = []
-_errors([{}]) = []
-// 解析：全程用 IF/ELSE IF 分支，禁用 CONTINUE —— VL 运行时 C-style FOR 内 continue 不执行自增表达式，会在首个 CONTINUE 行（如表头）原地死循环
-FOR (_i(INT) = 0; _i < _lines.length; _i++)
--_line(STRING) = _lines[_i].trim()
--_isHeader(BOOL) = (_i == 0 && _line.toLowerCase().indexOf("deptcode") == 0)
--IF _line != "" && !_isHeader
---_cols([STRING]) = _line.split(",")
---_code(STRING) = _cols.length > 0 ? _cols[0].trim() : ""
---_name(STRING) = _cols.length > 1 ? _cols[1].trim() : ""
---_parent(STRING) = _cols.length > 2 ? _cols[2].trim() : ""
---_sortStr(STRING) = _cols.length > 3 ? _cols[3].trim() : ""
---_statusVal(STRING) = _cols.length > 4 ? _cols[4].trim() : "active"
---IF _statusVal == ""
----_statusVal = "active"
---IF _code == "" || _name == ""
----_errors.push({line:(_i + 1),deptCode:_code,reason:"deptCode 与 name 必填"})
---ELSE IF _statusVal != "active" && _statusVal != "disabled"
----_errors.push({line:(_i + 1),deptCode:_code,reason:("非法 status: " + _statusVal)})
---ELSE IF _parent == _code
----_errors.push({line:(_i + 1),deptCode:_code,reason:"parentDeptCode 不得指向自身"})
---ELSE IF _codes.indexOf(_code) >= 0
----_errors.push({line:(_i + 1),deptCode:_code,reason:"CSV 内重复编码"})
---ELSE
----_codes.push(_code)
----_names.push(_name)
----_parents.push(_parent)
----_sorts.push(_sortStr != "" ? (_sortStr * 1) : 0)
----_statuses.push(_statusVal)
----_lineNos.push(_i + 1)
-IF _codes.length == 0 && _errors.length == 0
--RETURN {success:false,message:"no data rows",total:0,created:0,failed:0,errors:[]}
-// 预扫描：既有目录查重（_dbDup）+ 既有 active 父节点存在性（_parentDbActive，逐行预计算，使多趟插入内零 DB 调用）
-_dbDup([STRING]) = []
-_parentDbActive([BOOL]) = []
-FOR (_di(INT) = 0; _di < _codes.length; _di++)
--<VirtualTable-Departments "depts">.count([["deptCode","eq",_codes[_di]]]) -> _dupCnt
--IF _dupCnt.count > 0
---_dbDup.push(_codes[_di])
--_pdbOk(BOOL) = false
--IF _parents[_di] != ""
---<VirtualTable-Departments "depts">.count([["deptCode","eq",_parents[_di]],["status","eq","active"]]) -> _pCnt
---IF _pCnt.count > 0
----_pdbOk = true
--_parentDbActive.push(_pdbOk)
-// 多趟依赖插入（拓扑顺序，全程 IF/ELSE 无 CONTINUE/BREAK）：父有效 = 空 / 既有 active 目录 / 本批已成功落位；
-// 父在本批但尚未落位 → 留待下一趟；父既不在本批也不在既有目录 → 立即判“父节点不存在”；
-// 多趟收敛后仍未落位 = CSV 内环路或父行本身导入失败 → 整行失败，绝不写入。
-// 趟数上限取 _codes.length + 1（最坏为一条 N 节点链，需 N 趟落位），收敛后多余趟为纯数组遍历、零 DB 调用。
-_done([STRING]) = []
-_failedIdx([INT]) = []
-_createdCount(INT) = 0
-FOR (_pass(INT) = 0; _pass <= _codes.length; _pass++)
--FOR (_k(INT) = 0; _k < _codes.length; _k++)
---_isDone(BOOL) = (_done.indexOf(_codes[_k]) >= 0)
---_isFailed(BOOL) = (_failedIdx.indexOf(_k) >= 0)
---IF !_isDone && !_isFailed
----IF _dbDup.indexOf(_codes[_k]) >= 0
-----_errors.push({line:_lineNos[_k],deptCode:_codes[_k],reason:"编码已存在于部门目录"})
-----_failedIdx.push(_k)
----ELSE
-----_pEmpty(BOOL) = (_parents[_k] == "")
-----_pDoneInBatch(BOOL) = (_done.indexOf(_parents[_k]) >= 0)
-----_pInBatch(BOOL) = (_codes.indexOf(_parents[_k]) >= 0)
-----IF _pEmpty || _parentDbActive[_k] || _pDoneInBatch
------<VirtualTable-Departments "depts">.insert({deptCode:_codes[_k],parentDeptCode:_parents[_k],name:_names[_k],status:_statuses[_k],sort:_sorts[_k]}) -> _insR
------IF !_insR.success
-------_errors.push({line:_lineNos[_k],deptCode:_codes[_k],reason:("插入失败: " + _insR.message)})
-------_failedIdx.push(_k)
------ELSE
-------_done.push(_codes[_k])
-------_createdCount = _createdCount + 1
-----ELSE IF !_pInBatch
------_errors.push({line:_lineNos[_k],deptCode:_codes[_k],reason:("父节点不存在: " + _parents[_k])})
------_failedIdx.push(_k)
-// 多趟后仍未落位的行 = CSV 内环路，或父行在同批中本身导入失败 → 整行失败
-FOR (_m(INT) = 0; _m < _codes.length; _m++)
--_mDone(BOOL) = (_done.indexOf(_codes[_m]) >= 0)
--_mFailed(BOOL) = (_failedIdx.indexOf(_m) >= 0)
--IF !_mDone && !_mFailed
---_errors.push({line:_lineNos[_m],deptCode:_codes[_m],reason:"父链无法落位（CSV 内环路或父行导入失败）"})
-RETURN {success:(_errors.length == 0),message:(_errors.length == 0 ? "" : ("导入完成，" + _errors.length + " 行失败")),total:(_createdCount + _errors.length),created:_createdCount,failed:_errors.length,errors:_errors}

// ── UC-008A：OAuth provider 管理（缺口3）────────────────────────────────────────────────

// @meta service description:"内部：OAuth provider 管理列表。返回对象逐字段构造，绝不含 clientSecret*（仅以 hasSecret 标记是否已设置）。"
SERVICE ListOAuthProviders();RETURN {success:BOOL,providers:[{}]}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,providers:[]}
-<VirtualTable-OAuthProvidersAdmin "oauthAdmin">.select(null,[["_id","asc"]],[0,100],null) -> _r
-_list([{}]) = []
-FOR (_p, _pi) IN _r.dataArray
--_list.push({id:_p._id,providerCode:_p.providerCode,enabled:(_p.enabled == true),displayName:(_p.displayName ? _p.displayName : ""),buttonText:(_p.buttonText ? _p.buttonText : ""),loginUrl:(_p.loginUrl ? _p.loginUrl : ""),clientId:(_p.clientId ? _p.clientId : ""),scope:(_p.scope ? _p.scope : ""),callbackUrl:(_p.callbackUrl ? _p.callbackUrl : ""),hasSecret:(_p.clientSecretEncrypted != "" && _p.clientSecretEncrypted != NULL)})
-RETURN {success:true,providers:_list}

// @meta service description:"内部：新建/更新 OAuth provider（providerCode 唯一定位；不含 secret，secret 走 SetOAuthProviderSecret）。UC-008A：需 admin 角色。"
SERVICE UpsertOAuthProvider(providerCode(STRING),enabled(BOOL),displayName(STRING),buttonText(STRING),loginUrl(STRING),clientId(STRING),scope(STRING),callbackUrl(STRING));RETURN {success:BOOL,message:STRING,action:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,action:"denied"}
-IF providerCode == "" || providerCode == NULL
--RETURN {success:false,message:"providerCode required",action:""}
-<VirtualTable-OAuthProvidersAdmin "oauthAdmin">.select([["providerCode","eq",providerCode]],[["_id","asc"]],[0,1],null) -> _exist
-IF _exist.dataArray.length == 0
--<VirtualTable-OAuthProvidersAdmin "oauthAdmin">.insert({providerCode:providerCode,enabled:enabled,displayName:(displayName ? displayName : ""),buttonText:(buttonText ? buttonText : ""),loginUrl:(loginUrl ? loginUrl : ""),clientId:(clientId ? clientId : ""),scope:(scope ? scope : ""),callbackUrl:(callbackUrl ? callbackUrl : "")}) -> _ins
--IF !_ins.success
---RETURN {success:false,message:_ins.message,action:"insert_failed"}
--RETURN {success:true,message:"",action:"insert"}
-_rowId(INT) = _exist.dataArray[0]._id
-<VirtualTable-OAuthProvidersAdmin "oauthAdmin">.update([["_id","eq",_rowId]],[["enabled","set",enabled],["displayName","set",(displayName ? displayName : "")],["buttonText","set",(buttonText ? buttonText : "")],["loginUrl","set",(loginUrl ? loginUrl : "")],["clientId","set",(clientId ? clientId : "")],["scope","set",(scope ? scope : "")],["callbackUrl","set",(callbackUrl ? callbackUrl : "")]],1) -> _upd
-IF !_upd.success
--RETURN {success:false,message:_upd.message,action:"update_failed"}
-RETURN {success:true,message:"",action:"update"}

// @meta service description:"内部：设置 provider 的 clientSecret。不可逆保护：仅存 sha256(secret) 摘要（带 sha256: 前缀），任何接口不回显明文也无法还原；phase-1 未实现真实 OAuth 交换流程，待平台密钥管理上线后再切可逆加密存储。UC-008A：需 admin 角色。"
SERVICE SetOAuthProviderSecret(providerCode(STRING),clientSecret(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-IF providerCode == "" || providerCode == NULL || clientSecret == "" || clientSecret == NULL
--RETURN {success:false,message:"providerCode and clientSecret required"}
-<VirtualTable-OAuthProvidersAdmin "oauthAdmin">.count([["providerCode","eq",providerCode]]) -> _cnt
-IF _cnt.count == 0
--RETURN {success:false,message:"provider not found"}
-<BackendSecurityToolkit "securityToolkit">.sha256(clientSecret, "hex", "") -> _hash
-IF !_hash.success
--RETURN {success:false,message:("hash failed: " + _hash.message)}
-<VirtualTable-OAuthProvidersAdmin "oauthAdmin">.update([["providerCode","eq",providerCode]],[["clientSecretEncrypted","set",("sha256:" + _hash.hash)]],1) -> _upd
-IF !_upd.success
--RETURN {success:false,message:_upd.message}
-RETURN {success:true,message:""}

// ── UC-008A：注册审批（缺口4）───────────────────────────────────────────────────────────

// @meta service description:"内部：审批通过待审核用户（pendingApproval -> active），写审计 approval/approve。仅 pendingApproval 状态可审批。"
SERVICE ApproveUser(userId(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-_userIdInt(INT) = userId * 1
-<VirtualTable-AuthUsers "authUsers">.select([["_id","eq",_userIdInt]],null,[0,1],null) -> _row
-IF _row.dataArray.length == 0
--RETURN {success:false,message:"user not found"}
-IF ("" + _row.dataArray[0].status) != "pendingApproval"
--RETURN {success:false,message:("user status is '" + _row.dataArray[0].status + "', not pendingApproval")}
-<VirtualTable-AuthUsers "authUsers">.update([["_id","eq",_userIdInt]],[["status","set","active"]],1) -> _upd
-IF !_upd.success || _upd.affect == 0
--RETURN {success:false,message:(_upd.message ? _upd.message : "update affected 0 rows")}
-writeAudit(userId, "", "password", "approval", "success", "approved") -> _a
-RETURN {success:true,message:""}

// @meta service description:"内部：拒绝待审核用户（pendingApproval -> disabled），并撤销其全部 token（与禁用口径一致），写审计 approval/reject。"
SERVICE RejectUser(userId(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-_userIdInt(INT) = userId * 1
-<VirtualTable-AuthUsers "authUsers">.select([["_id","eq",_userIdInt]],null,[0,1],null) -> _row
-IF _row.dataArray.length == 0
--RETURN {success:false,message:"user not found"}
-IF ("" + _row.dataArray[0].status) != "pendingApproval"
--RETURN {success:false,message:("user status is '" + _row.dataArray[0].status + "', not pendingApproval")}
-<VirtualTable-AuthUsers "authUsers">.update([["_id","eq",_userIdInt]],[["status","set","disabled"]],1) -> _upd
-IF !_upd.success || _upd.affect == 0
--RETURN {success:false,message:(_upd.message ? _upd.message : "update affected 0 rows")}
-resolveUserCenterNid() -> _uc
-IF _uc.success
--<TokenIssuer "tokenIssuer">.revokeAllUserTokens(userId, _uc.realmCode) -> _rv
-writeAudit(userId, "", "password", "approval", "success", "rejected") -> _a
-RETURN {success:true,message:""}

// ── 公开 HTTP 接口（用户权限中心方案 §公开服务边界）──────────────────────────────────────
// 公开服务只面向：业务项目、ClientUserCenter 组件、外部集成。
// 平台路由 /auth/api/<name> → 这些 PUBLIC_SERVICE；项目和 ClientUserCenter 不配置 nid、URL，
// 由平台按当前区服 / 组织上下文路由到默认用户权限中心。
// A/B 内部协作走 bindOnly + 同 gid 共享 DB，不通过这些公开接口。
//
// 实现策略（2026-06-12 UC-PM-001/002 运行时定位，结论修订）：
// - 【平台墙·未解】<UserStore> facade 在 PUBLIC_SERVICE 请求上下文整体不可执行：
//   verifyUserByPassword 与 createUserByPassword 都返回空 {}（HTTP 200, size 2），
//   且最小诊断（仅调一次 facade 即 RETURN）在 facade 调用处就吐空、到不了自己的 RETURN。
//   与嵌套层级无关：inline / 转内部 SERVICE / 转 internal METHOD 三种包装全部 {}。
//   对照：<TokenIssuer> 在 PUBLIC 正常（logout getCurrentSession 返结构化 JSON），
//   VirtualTable 读写 / internal METHOD / SYSENV 在 PUBLIC 均正常（GetClientConfig 为证）。
//   ⇒ 这是平台/extgo/sys 侧问题，VL 应用层无法绕过（VL 无 crypto，凭证校验/建号必须经 UserStore）。
//   PasswordLogin / Register 保持 delegation 形态，平台修复 UserStore-in-PUBLIC 后即自动生效。
//   公开 HTTP 账密链路在平台修复前不可用；产品主链路走前端登录页（App 上下文，UserStore 正常）。
// - 不碰 facade 的公开接口（GetClientConfig / Login / UserCenter / CurrentUser）保持 inline。

// @meta service description:"PUBLIC: /auth/api/password-login。账号密码登录。参数校验后转内部 SERVICE LoginByPassword 执行（UserStore/TokenIssuer facade 在 PUBLIC body 直调会吐空 body，见问题清单 A1），RETURN 显式逐字段组装。" params:({account:{description:"账号",required:true,example:"alice"},password:{description:"密码",required:true,example:"secret"}}) returns:({success:{description:"是否成功"},message:{description:"错误信息"},userId:{description:"用户 ID（STRING）"},tokenId:{description:"会话 token ID"},expiresAt:{description:"过期时间戳"},user:{description:"用户基础资料对象"},subjectInfo:{description:"subjectInfo 对象，含 roles / departmentIds 等"}}) auth:({required:false})
PUBLIC_SERVICE PasswordLogin(account(STRING),password(STRING));RETURN {success:BOOL,message:STRING,userId:STRING,tokenId:STRING,expiresAt:INT,user:{},subjectInfo:{}};EXPOSE {method:"POST",receive:"JSON",response:"JSON"}
-IF account == "" || account == NULL || password == "" || password == NULL
--RETURN {success:false,message:"account and password required",userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{}}
-loginByPasswordCore(account, password) -> _r
-RETURN {success:_r.success,message:_r.message,userId:_r.userId,tokenId:_r.tokenId,expiresAt:_r.expiresAt,user:_r.user,subjectInfo:_r.subjectInfo}

// @meta service description:"PUBLIC: /auth/api/register。账号密码注册。需要审核时 needApproval=true 不签发 token。参数校验后转内部 SERVICE SignupByPassword 执行（同 A1：facade 不能在 PUBLIC body 直调）。" params:({account:{description:"账号",required:true,example:"alice"},password:{description:"密码",required:true,example:"secret"},nickname:{description:"昵称",required:true,example:"Alice"}}) returns:({success:{description:"是否成功"},message:{description:"错误信息"},userId:{description:"用户 ID"},tokenId:{description:"会话 token ID（needApproval=true 时为空）"},expiresAt:{description:"过期时间戳（needApproval=true 时为 0）"},user:{description:"用户基础资料"},subjectInfo:{description:"subjectInfo 对象（needApproval=true 时为空）"},needApproval:{description:"是否需要审核"}}) auth:({required:false})
PUBLIC_SERVICE Register(account(STRING),password(STRING),nickname(STRING));RETURN {success:BOOL,message:STRING,userId:STRING,tokenId:STRING,expiresAt:INT,user:{},subjectInfo:{},needApproval:BOOL};EXPOSE {method:"POST",receive:"JSON",response:"JSON"}
-IF account == "" || account == NULL || password == "" || password == NULL || nickname == "" || nickname == NULL
--RETURN {success:false,message:"account, password and nickname required",userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{},needApproval:false}
-SignupByPassword(account, password, nickname) -> _r
-RETURN {success:_r.success,message:_r.message,userId:_r.userId,tokenId:_r.tokenId,expiresAt:_r.expiresAt,user:_r.user,subjectInfo:_r.subjectInfo,needApproval:_r.needApproval}

// @meta service description:"PUBLIC: /auth/api/logout。撤销当前会话 token。转内部 SERVICE LogoutInternal 执行（同 A1：TokenIssuer facade 不能在 PUBLIC body 直调）。" returns:({success:{description:"是否成功"},message:{description:"错误信息"}}) auth:({required:false})
PUBLIC_SERVICE Logout();RETURN {success:BOOL,message:STRING};EXPOSE {method:"POST",receive:"JSON",response:"JSON"}
-LogoutInternal() -> _r
-RETURN {success:_r.success,message:_r.message}

// @meta service description:"PUBLIC: /auth/api/current-user。读取当前后端登录用户。inline 实现。" returns:({isLogin:{description:"是否登录"},userId:{description:"当前用户 ID（未登录为空）"},userInfo:{description:"用户基础资料对象（未登录为空）"}}) auth:({required:false})
PUBLIC_SERVICE CurrentUser();RETURN {isLogin:BOOL,userId:STRING,userInfo:{}};EXPOSE {method:"GET",receive:"JSON",response:"JSON"}
-currentCaller() -> _cc
-RETURN {isLogin:_cc.isLogin,userId:_cc.userId,userInfo:_cc.userInfo}

// @meta service description:"PUBLIC: /auth/api/verifycurrentuser。UC-007A：供 B（权限中心）等服务端验证『当前请求所携带 A 会话』的稳定身份入口。契约：调用方服务端把终端用户原始请求的认证凭据（Cookie / ih5 token 头）原样转发到本端点；A 在自己 realm 解析会话（TokenIssuer 无按任意 token 校验能力，平台会话只能随请求上下文解析），返回权威 userId / accountStatus（读 auth_users 状态列，非 token 内缓存）/ subjectInfo（roles/departmentIds 实时口径）。未登录返回 isLogin:false 而非错误。" returns:({success:{description:"接口是否成功执行"},isLogin:{description:"该凭据是否对应有效 A 会话"},userId:{description:"用户 ID（未登录为空）"},accountStatus:{description:"权威账号状态：active / pendingApproval / disabled（未登录为空）"},subjectInfo:{description:"实时 subjectInfo（roles/departmentIds 等；未登录为空对象）"},message:{description:"错误信息"}}) auth:({required:false})
PUBLIC_SERVICE VerifyCurrentUser();RETURN {success:BOOL,isLogin:BOOL,userId:STRING,accountStatus:STRING,subjectInfo:{},message:STRING};EXPOSE {method:"POST",receive:"JSON",response:"JSON"}
-currentCaller() -> _cc
-IF !_cc.isLogin
--RETURN {success:true,isLogin:false,userId:"",accountStatus:"",subjectInfo:{},message:""}
-_uid(STRING) = _cc.userId
-_uidInt(INT) = _uid * 1
-<VirtualTable-AuthUsers "authUsers">.select([["_id","eq",_uidInt]],null,[0,1],null) -> _row
-resolveUserCenterNid() -> _uc
-_nidForSubject(STRING) = _uc.success ? _uc.nid : ("" + SYSENV.currentApp.nid)
-IF _row.dataArray.length == 0
--// Local rustBase UserStore is memory-backed and can issue a valid token before the
--// artifact business table has a matching AuthUsers row. Keep cloud fail-closed,
--// but let localhost token verification report the current session as logged in.
--_verifyHost(STRING) = SYSENV.currentApp.previewDomain
--_isLocalVerify(BOOL) = (_verifyHost.indexOf("127.0.0.1") == 0 || _verifyHost.indexOf("localhost") == 0)
--IF _isLocalVerify
---buildSubjectInfo(_uid, _nidForSubject, _cc.userInfo, "active") -> _localSubj
---RETURN {success:true,isLogin:true,userId:_uid,accountStatus:"active",subjectInfo:_localSubj.subjectInfo,message:""}
--RETURN {success:true,isLogin:false,userId:_uid,accountStatus:"",subjectInfo:{},message:"session user not found in auth_users"}
-_acctStatus(STRING) = _row.dataArray[0].status ? ("" + _row.dataArray[0].status) : "active"
-buildSubjectInfo(_uid, _nidForSubject, _cc.userInfo, _acctStatus) -> _subj
-RETURN {success:true,isLogin:true,userId:_uid,accountStatus:_acctStatus,subjectInfo:_subj.subjectInfo,message:""}

// @meta service description:"PUBLIC: /auth/login。返回用户中心前端登录入口 URL，由 ClientUserCenter 前端跳转。runtime 不再走 CUSTOM_RETURN 302（验证为返回空 body），改 JSON {success, redirectUrl}。URL 经 resolveFrontendUrls（配置优先，短码 fallback，见 A1）。" returns:({success:{description:"是否成功"},redirectUrl:{description:"用户中心前端登录入口 URL"}}) auth:({required:false})
PUBLIC_SERVICE Login();RETURN {success:BOOL,redirectUrl:STRING};EXPOSE {method:"GET",receive:"JSON",response:"JSON"}
-resolveFrontendUrls() -> _urls
-RETURN {success:true,redirectUrl:_urls.userCenterUrl}

// @meta service description:"PUBLIC: /auth/user-center。返回用户中心前端主页 URL，由 ClientUserCenter 前端跳转。URL 经 resolveFrontendUrls（配置优先，短码 fallback，见 A1）。" returns:({success:{description:"是否成功"},redirectUrl:{description:"用户中心前端主页 URL"}}) auth:({required:false})
PUBLIC_SERVICE UserCenter();RETURN {success:BOOL,redirectUrl:STRING};EXPOSE {method:"GET",receive:"JSON",response:"JSON"}
-resolveFrontendUrls() -> _urls
-RETURN {success:true,redirectUrl:_urls.userCenterUrl}

// @meta service description:"PUBLIC: /auth/api/getclientconfig。给 ClientUserCenterCP 拉接入配置。UC-007A：支持多用户中心——按 ProjectUserCenterBindings(中心化绑定) -> 项目配置 configuredNid -> 默认用户中心 的固定顺序解析实例，返回解析后的 A/B 地址、backend nid 与 bindingSource(projectBinding/projectConfig/defaultUserCenter)。projectId/configuredNid 均可不传（向后兼容：不传 projectId 跳过绑定解析，不传 configuredNid 走默认用户中心）。" params:({env:{description:"环境，phase-1 仅 preview",required:false,example:"preview"},userSpaceCode:{description:"用户空间，phase-1 仅 default（内部固定，非产品维度）",required:false,example:"default"},projectId:{description:"业务项目标识；传入后启用中心化绑定解析",required:false,example:"ProjectAccessTest"},configuredNid:{description:"项目自身配置的 userPermissionCenterNid（字符串数字）；无中心化绑定时按它解析",required:false,example:"12206000"}}) returns:({success:{description:"是否成功"},message:{description:"错误信息"},bindingSource:{description:"解析来源：projectBinding / projectConfig / defaultUserCenter"},userCenterCode:{description:"解析到的用户中心实例 code"},userPermissionCenterNid:{description:"解析后的用户中心后端 nid"},userCenterUrl:{description:"解析后的用户中心前端 URL"},permissionCenterUrl:{description:"解析后的权限中心前端 URL"},authApiBase:{description:"auth API base 前缀（按解析 nid 推导）"},permissionApiBase:{description:"permission API base 前缀（按解析 B nid 推导）"},authProviders:{description:"启用的第三方登录 provider 公开列表，无则 []，绝不含 clientSecret*"},env:{description:"回声 env"},userSpaceCode:{description:"回声 userSpaceCode"}}) auth:({required:false})
PUBLIC_SERVICE GetClientConfig(env(STRING),userSpaceCode(STRING),projectId(STRING),configuredNid(STRING));RETURN {success:BOOL,message:STRING,bindingSource:STRING,userCenterCode:STRING,userPermissionCenterNid:STRING,userCenterUrl:STRING,permissionCenterUrl:STRING,authApiBase:STRING,permissionApiBase:STRING,authProviders:[{}],env:STRING,userSpaceCode:STRING};EXPOSE {method:"POST",receive:"JSON",response:"JSON"}
-_envValue(STRING) = (env != "" && env != NULL) ? env : "preview"
-_space(STRING) = (userSpaceCode != "" && userSpaceCode != NULL) ? userSpaceCode : "default"
-IF _envValue != "preview"
--RETURN {success:false,message:("env '" + _envValue + "' not supported in phase-1"),bindingSource:"",userCenterCode:"",userPermissionCenterNid:"",userCenterUrl:"",permissionCenterUrl:"",authApiBase:"",permissionApiBase:"",authProviders:[],env:_envValue,userSpaceCode:_space}
-IF _space != "default"
--RETURN {success:false,message:("userSpaceCode '" + _space + "' not supported in phase-1"),bindingSource:"",userCenterCode:"",userPermissionCenterNid:"",userCenterUrl:"",permissionCenterUrl:"",authApiBase:"",permissionApiBase:"",authProviders:[],env:_envValue,userSpaceCode:_space}
-_backendNid(INT) = SYSENV.currentApp.nid
-IF _backendNid == 0
--RETURN {success:false,message:"SYSENV.currentApp.nid unavailable",bindingSource:"",userCenterCode:"",userPermissionCenterNid:"",userCenterUrl:"",permissionCenterUrl:"",authApiBase:"",permissionApiBase:"",authProviders:[],env:_envValue,userSpaceCode:_space}
-_host(STRING) = SYSENV.currentApp.previewDomain
-_isLocalHost(BOOL) = (_host.indexOf("127.0.0.1") == 0 || _host.indexOf("localhost") == 0)
-_scheme(STRING) = _isLocalHost ? "http://" : "https://"
-// UC-007A：实例解析（绑定 -> 项目配置 -> 默认）
-_cfgNidInt(INT) = (configuredNid != "" && configuredNid != NULL) ? (configuredNid * 1) : 0
-_pidValue(STRING) = (projectId != "" && projectId != NULL) ? projectId : ""
-resolveUserCenterInstance(_pidValue, _envValue, _cfgNidInt) -> _inst
-_ucNid(STRING) = "" + _inst.ucNid
-_authApiBase(STRING) = _scheme + _host + "/api/" + _ucNid + "/usercenterservice"
-_pcNidStr(STRING) = "" + _inst.pcNid
-_permissionApiBase(STRING) = _scheme + _host + "/api/" + _pcNidStr + "/permissioncenterservice"
-// authProviders：仅公开字段（providerCode/displayName/buttonText/loginUrl/enabled），
-// 受控视图 "oauthProviders" 物理上不含 clientSecret*，无泄漏面。
-<VirtualTable-OAuthProviders "oauthProviders">.select([["enabled","eq",true]],[["_id","asc"]],[0,50],null) -> _provResult
-_providers([{}]) = []
-FOR (_p, _pi) IN _provResult.dataArray
--_providers.push({providerCode:_p.providerCode,displayName:(_p.displayName ? _p.displayName : ""),buttonText:(_p.buttonText ? _p.buttonText : ""),loginUrl:(_p.loginUrl ? _p.loginUrl : ""),enabled:true})
-// UC-A-CONFIG-FAILCLOSED-20260617-002：前端登录入口 URL 缺失/非法时 fail-closed —— 返回 success:false 且清空跳转 URL，不再 success:true 带空 URL 让接入方跳空地址（故障后移到业务项目）
-_ucFrontUrl(STRING) = ("" + _inst.ucFrontendUrl)
-_pcFrontUrl(STRING) = ("" + _inst.pcFrontendUrl)
-IF (_ucFrontUrl == "" || _ucFrontUrl == "null") && _isLocalHost
--_ucFrontUrl = _scheme + _host + SYSENV.currentApp.playPath
-IF _pcFrontUrl == "" || _pcFrontUrl == "null"
--_pcFrontUrl = _ucFrontUrl
-IF _ucFrontUrl == "" || _ucFrontUrl == "null"
--RETURN {success:false,message:("用户中心登录入口未配置：请在用户中心「实例与绑定」为后端 nid " + _ucNid + " 配置 userCenterFrontendUrl 后再接入"),bindingSource:_inst.bindingSource,userCenterCode:_inst.userCenterCode,userPermissionCenterNid:_ucNid,userCenterUrl:"",permissionCenterUrl:"",authApiBase:"",permissionApiBase:"",authProviders:[],env:_envValue,userSpaceCode:_space}
-RETURN {success:true,message:"",bindingSource:_inst.bindingSource,userCenterCode:_inst.userCenterCode,userPermissionCenterNid:_ucNid,userCenterUrl:_ucFrontUrl,permissionCenterUrl:_pcFrontUrl,authApiBase:_authApiBase,permissionApiBase:_permissionApiBase,authProviders:_providers,env:_envValue,userSpaceCode:_space}

// ── 内部 SERVICE ───────────────────────────────────────────────────────────────────────

// @meta service description:"账号密码注册。requireApproval=true 时账号置 pendingApproval 不签发 token；否则 active 注册即登录。"
SERVICE SignupByPassword(account(STRING),password(STRING),nickname(STRING));RETURN {success:BOOL,message:STRING,userId:STRING,tokenId:STRING,expiresAt:INT,user:{},subjectInfo:{},needApproval:BOOL}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{},needApproval:false}
-_userCenterNid(STRING) = _ucResult.nid
-_realmCode(STRING) = _ucResult.realmCode
-_registrationOpen(BOOL) = _ucResult.registrationOpen
-_requireApproval(BOOL) = _ucResult.requireApproval
-_defaultRoleCode(STRING) = _ucResult.defaultRoleCode
-_sessionDuration(INT) = _ucResult.sessionDuration
-_maxConcurrent(INT) = _ucResult.maxConcurrent
-IF !_registrationOpen
--writeAudit("", account, "password", "register", "failure", "registration_closed") -> _a
--RETURN {success:false,message:"当前系统未开放注册，如需开通请联系系统管理员",userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{},needApproval:false}
-<UserStore "userStore">.createUserByPassword(account, password, nickname, NULL, NULL, NULL, NULL) -> _signupResult
-IF !_signupResult.success
--writeAudit("", account, "password", "register", "failure", _signupResult.message) -> _a
--RETURN {success:false,message:_signupResult.message,userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{},needApproval:false}
-_userIdStr(STRING) = "" + _signupResult.userId
-// 受控假设：UserStore.userId 当前等于 auth_users._id 的字符串形式，phase-1 通过 STRING->INT 转换索引 _id；
-// 后续平台若提供 UserStore.setUserStatus / UserStore.findByStringId，应优先切换并删除此转换。
-_userIdInt(INT) = _userIdStr * 1
-_targetStatus(STRING) = _requireApproval ? "pendingApproval" : "active"
-<VirtualTable-AuthUsers "authUsers">.update([["_id","eq",_userIdInt]],[["status","set",_targetStatus]],1) -> _statusResult
-IF _defaultRoleCode != "" && _defaultRoleCode != NULL
--<VirtualTable-UserRoles "roles">.count([["roleCode","eq",_defaultRoleCode],["status","eq","active"]]) -> _roleExist
--IF _roleExist.count > 0
---<VirtualTable-UserRoleAssignments "roleAssign">.insert({userId:_userIdStr,roleCode:_defaultRoleCode,assignedBy:"system",status:"active"}) -> _assignResult
-writeAudit(_userIdStr, account, "password", "register", "success", "") -> _a
-IF _requireApproval
--RETURN {success:true,message:"pending approval",userId:_userIdStr,tokenId:"",expiresAt:0,user:_signupResult.user,subjectInfo:{},needApproval:true}
-buildSubjectInfo(_userIdStr, _userCenterNid, _signupResult.subjectInfo, "active") -> _subj
-<TokenIssuer "tokenIssuer">.generateLoginToken(_userIdStr, _realmCode, _subj.subjectInfo, _signupResult.user, _maxConcurrent, _sessionDuration) -> _tokenResult
-IF !_tokenResult.success
--RETURN {success:false,message:_tokenResult.message,userId:_userIdStr,tokenId:"",expiresAt:0,user:_signupResult.user,subjectInfo:_subj.subjectInfo,needApproval:false}
-RETURN {success:true,message:"",userId:_userIdStr,tokenId:_tokenResult.tokenId,expiresAt:_tokenResult.expiresAt,user:_signupResult.user,subjectInfo:_subj.subjectInfo,needApproval:false}

// @meta service description:"读取初始化状态：clean 系统（尚无任何用户）需要先创建首个超级管理员，供首次运行引导到创建管理员页。"
PUBLIC_SERVICE GetSetupStatus();RETURN {success:BOOL,needsSetup:BOOL};EXPOSE {method:"POST",receive:"JSON",response:"JSON"}
-<VirtualTable-AuthUsers "statusAu">.count(null) -> _ucnt
-RETURN {success:true,needsSetup:(_ucnt.count == 0)}

// @meta service description:"首次初始化：系统尚无任何用户时，部署者自填账号密码创建首个超管账号（createUserByPassword 正确哈希）、赋 admin 角色并签发登录 token；已有用户即拒绝，防止匿名再建。super 授权与 pc/uc 配置由前端随后调 InitializePermissionCenter / EnsureDefaultUserCenter 完成（首跑无超管时开放）。"
PUBLIC_SERVICE SetupFirstSuperAdmin(account(STRING),password(STRING),nickname(STRING));RETURN {success:BOOL,message:STRING,userId:STRING,tokenId:STRING,expiresAt:INT,user:{},subjectInfo:{}};EXPOSE {method:"POST",receive:"JSON",response:"JSON"}
-IF account == "" || password == "" || nickname == ""
--RETURN {success:false,message:"请填写管理员账号、密码与名称",userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{}}
-<VirtualTable-AuthUsers "setupGuard">.count(null) -> _ucnt
-IF _ucnt.count > 0
--RETURN {success:false,message:"系统已存在用户，无法再以初始化方式创建超级管理员",userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{}}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{}}
-<UserStore "userStore">.createUserByPassword(account, password, nickname, NULL, NULL, NULL, NULL) -> _signupResult
-IF !_signupResult.success
--RETURN {success:false,message:_signupResult.message,userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{}}
-_userIdStr(STRING) = "" + _signupResult.userId
-_userIdInt(INT) = _userIdStr * 1
-<VirtualTable-AuthUsers "setupAu">.update([["_id","eq",_userIdInt]],[["status","set","active"]],1) -> _statusResult
-<VirtualTable-UserRoleAssignments "setupRa">.count([["userId","eq",_userIdStr],["roleCode","eq","admin"],["status","eq","active"]]) -> _hasRole
-IF _hasRole.count == 0
--<VirtualTable-UserRoleAssignments "setupRa2">.insert({userId:_userIdStr,roleCode:"admin",assignedBy:"setup",status:"active"}) -> _raIns
-buildSubjectInfo(_userIdStr, _ucResult.nid, _signupResult.subjectInfo, "active") -> _subj
-<TokenIssuer "tokenIssuer">.generateLoginToken(_userIdStr, _ucResult.realmCode, _subj.subjectInfo, _signupResult.user, _ucResult.maxConcurrent, _ucResult.sessionDuration) -> _tokenResult
-IF !_tokenResult.success
--RETURN {success:false,message:_tokenResult.message,userId:_userIdStr,tokenId:"",expiresAt:0,user:_signupResult.user,subjectInfo:_subj.subjectInfo}
-writeAudit(_userIdStr, account, "password", "setup-admin", "success", "") -> _a
-RETURN {success:true,message:"",userId:_userIdStr,tokenId:_tokenResult.tokenId,expiresAt:_tokenResult.expiresAt,user:_signupResult.user,subjectInfo:_subj.subjectInfo}

// @meta service description:"账号密码登录。账号 disabled / pendingApproval 时不签发 token。"
SERVICE LoginByPassword(account(STRING),password(STRING));RETURN {success:BOOL,message:STRING,userId:STRING,tokenId:STRING,expiresAt:INT,user:{},subjectInfo:{}}
-loginByPasswordCore(account, password) -> _r
-RETURN _r

// @meta service description:"读取当前后端登录用户。未登录返回 isLogin=false。"
SERVICE GetCurrentUser();RETURN {isLogin:BOOL,userId:STRING,userInfo:{}}
-currentCaller() -> _cc
-RETURN {isLogin:_cc.isLogin,userId:_cc.userId,userInfo:_cc.userInfo}

// @meta service description:"内部登出。撤销当前会话 token。供用户中心自身页面（ProfilePage 等）调用。公开 HTTP 入口走 PUBLIC_SERVICE Logout。"
SERVICE LogoutInternal();RETURN {success:BOOL,message:STRING}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message}
-<TokenIssuer "tokenIssuer">.getCurrentSession(_ucResult.realmCode) -> _sessionResult
-IF !_sessionResult.success
--RETURN {success:false,message:"no active session"}
-<TokenIssuer "tokenIssuer">.revokeToken(_sessionResult.session.tokenId) -> _revokeResult
-RETURN {success:_revokeResult.success,message:_revokeResult.message}

// @meta service description:"用户列表（管理后台）。通过受控 VirtualTable-AuthUsers 只读访问。UC-008A：支持 status 服务端过滤（active/pendingApproval/disabled，空=全部），审批队列直接用 status=pendingApproval 拉取。"
SERVICE ListUsers(keyword(STRING),status(STRING),offset(INT),limit(INT));RETURN {success:BOOL,users:[{}],total:INT,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,users:[],total:0,message:_guard.message}
-_safeLimit(INT) = (limit > 0 && limit <= 200) ? limit : 50
-_safeOffset(INT) = offset > 0 ? offset : 0
-_trimmed(STRING) = keyword ? keyword : ""
-_statusVal(STRING) = status ? status : ""
-IF _trimmed == "" && _statusVal == ""
--<VirtualTable-AuthUsers "authUsers">.count(null) -> _cnt0
--<VirtualTable-AuthUsers "authUsers">.select(null,[["_id","desc"]],[_safeOffset,_safeLimit],null) -> _list0
--RETURN {success:true,users:_list0.dataArray,total:_cnt0.count,message:""}
-IF _trimmed == ""
--<VirtualTable-AuthUsers "authUsers">.count([["status","eq",_statusVal]]) -> _cnt1
--<VirtualTable-AuthUsers "authUsers">.select([["status","eq",_statusVal]],[["_id","desc"]],[_safeOffset,_safeLimit],null) -> _list1
--RETURN {success:true,users:_list1.dataArray,total:_cnt1.count,message:""}
-IF _statusVal == ""
--<VirtualTable-AuthUsers "authUsers">.count([["OR",["nickname","like",_trimmed],["display_name","like",_trimmed],["primary_email","like",_trimmed],["primary_phone","like",_trimmed]]]) -> _cnt2
--<VirtualTable-AuthUsers "authUsers">.select([["OR",["nickname","like",_trimmed],["display_name","like",_trimmed],["primary_email","like",_trimmed],["primary_phone","like",_trimmed]]],[["_id","desc"]],[_safeOffset,_safeLimit],null) -> _list2
--RETURN {success:true,users:_list2.dataArray,total:_cnt2.count,message:""}
-<VirtualTable-AuthUsers "authUsers">.count([["status","eq",_statusVal],["OR",["nickname","like",_trimmed],["display_name","like",_trimmed],["primary_email","like",_trimmed],["primary_phone","like",_trimmed]]]) -> _cnt3
-<VirtualTable-AuthUsers "authUsers">.select([["status","eq",_statusVal],["OR",["nickname","like",_trimmed],["display_name","like",_trimmed],["primary_email","like",_trimmed],["primary_phone","like",_trimmed]]],[["_id","desc"]],[_safeOffset,_safeLimit],null) -> _list3
-RETURN {success:true,users:_list3.dataArray,total:_cnt3.count,message:""}

// @meta service description:"按 ID 查询单个用户。"
SERVICE GetUserById(userId(STRING));RETURN {success:BOOL,message:STRING,user:{}}
-currentCaller() -> _cc
-IF !(_cc.isLogin && _cc.userId == userId)
--requireAdminRole() -> _guard
--IF !_guard.ok
---RETURN {success:false,message:_guard.message,user:{}}
-<UserStore "userStore">.getUserById(userId) -> _userResult
-IF !_userResult.success
--RETURN {success:false,message:_userResult.message,user:{}}
-RETURN {success:true,message:"",user:_userResult.user}

// @meta service description:"禁用用户。先用 UserStore.getUserById 校验存在，再更新 AuthUsers.status=disabled，并撤销该用户当前用户中心下全部 token。"
SERVICE DisableUser(userId(STRING));RETURN {success:BOOL,message:STRING,revokedCount:INT}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,revokedCount:0}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,revokedCount:0}
-<UserStore "userStore">.getUserById(userId) -> _userExist
-IF !_userExist.success
--RETURN {success:false,message:"user not found",revokedCount:0}
-// 受控假设：UserStore.userId 当前等于 auth_users._id 字符串形式。phase-1 通过 STRING->INT 转换索引 _id；
-// 平台后续提供 UserStore.setUserStatus(userId,status) 后应切换并删除此例外。
-_userIdInt(INT) = userId * 1
-<VirtualTable-AuthUsers "authUsers">.update([["_id","eq",_userIdInt]],[["status","set","disabled"]],1) -> _updateResult
-IF !_updateResult.success
--RETURN {success:false,message:_updateResult.message,revokedCount:0}
-<TokenIssuer "tokenIssuer">.revokeAllUserTokens(userId, _ucResult.realmCode) -> _revokeResult
-RETURN {success:true,message:"",revokedCount:_revokeResult.affectedCount}

// @meta service description:"启用用户。先用 UserStore.getUserById 校验存在，再更新 AuthUsers.status=active。"
SERVICE EnableUser(userId(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-<UserStore "userStore">.getUserById(userId) -> _userExist
-IF !_userExist.success
--RETURN {success:false,message:"user not found"}
-// 受控假设同 DisableUser。
-_userIdInt(INT) = userId * 1
-<VirtualTable-AuthUsers "authUsers">.update([["_id","eq",_userIdInt]],[["status","set","active"]],1) -> _updateResult
-RETURN {success:_updateResult.success,message:_updateResult.message}

// @meta service description:"角色目录列表（管理后台）。"
SERVICE ListRoles();RETURN {success:BOOL,roles:[{}],message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,roles:[],message:_guard.message}
-<VirtualTable-UserRoles "roles">.select(null,[["sort","asc"]],[0,200],null) -> _listResult
-RETURN {success:true,roles:_listResult.dataArray,message:""}

// @meta service description:"创建角色。"
SERVICE CreateRole(roleCode(STRING),name(STRING),description(STRING));RETURN {success:BOOL,message:STRING,roleCode:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,roleCode:""}
-<VirtualTable-UserRoles "roles">.count([["roleCode","eq",roleCode]]) -> _existResult
-IF _existResult.count > 0
--RETURN {success:false,message:"roleCode already exists",roleCode:roleCode}
-<VirtualTable-UserRoles "roles">.insert({roleCode:roleCode,name:name,description:description,status:"active",sort:0}) -> _insertResult
-RETURN {success:_insertResult.success,message:_insertResult.message,roleCode:roleCode}

// @meta service description:"更新角色显示名和描述。roleCode 不可修改。"
SERVICE UpdateRole(roleCode(STRING),name(STRING),description(STRING),sort(INT));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-<VirtualTable-UserRoles "roles">.count([["roleCode","eq",roleCode]]) -> _existResult
-IF _existResult.count == 0
--RETURN {success:false,message:"role not found"}
-<VirtualTable-UserRoles "roles">.update([["roleCode","eq",roleCode]],[["name","set",name],["description","set",description],["sort","set",sort]],1) -> _updateResult
-RETURN {success:_updateResult.success,message:_updateResult.message}

// @meta service description:"停用角色。停用后角色目录置 disabled；UserRoleAssignments 历史记录不动，调用 EnableRole 可直接恢复。
// 立即撤销当前用户中心下所有持有该角色的用户 token，让旧 token 不再携带已停用角色。
// phase-1 上限一次最多 revoke 1000 个用户；超过 1000 的尾巴依靠 buildSubjectInfo 在下次登录时过滤掉，
// 安全降级但有 token 延迟。TODO(phase-2): 分页批量 revoke，覆盖大客户场景。"
SERVICE DisableRole(roleCode(STRING));RETURN {success:BOOL,message:STRING,revokedUserCount:INT}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,revokedUserCount:0}
-// 自锁保护：admin 是管理守卫角色，停用会令全部管理面失守（或回退 bootstrap 全开放），禁止停用
-IF roleCode == "admin"
--RETURN {success:false,message:"role 'admin' is the management guard role and cannot be disabled",revokedUserCount:0}
-<VirtualTable-UserRoles "roles">.count([["roleCode","eq",roleCode]]) -> _existResult
-IF _existResult.count == 0
--RETURN {success:false,message:"role not found",revokedUserCount:0}
-<VirtualTable-UserRoles "roles">.update([["roleCode","eq",roleCode]],[["status","set","disabled"]],1) -> _updateResult
-IF !_updateResult.success
--RETURN {success:false,message:_updateResult.message,revokedUserCount:0}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:true,message:("role disabled but token revoke skipped: " + _ucResult.message),revokedUserCount:0}
-<VirtualTable-UserRoleAssignments "roleAssign">.select([["roleCode","eq",roleCode],["status","eq","active"]],null,[0,1000],["userId"]) -> _affected
-_revokedUserCount(INT) = 0
-FOR (_i(INT) = 0; _i < _affected.dataArray.length; _i++)
--<TokenIssuer "tokenIssuer">.revokeAllUserTokens(_affected.dataArray[_i].userId, _ucResult.realmCode) -> _r
--IF _r.success
---_revokedUserCount = _revokedUserCount + 1
-RETURN {success:true,message:"",revokedUserCount:_revokedUserCount}

// @meta service description:"恢复角色为 active。会撤销所有持有该角色的活跃 assignment 用户 token，让下次登录的 subjectInfo 重新包含该角色。
// phase-1 revoke 上限 1000 同 DisableRole。TODO(phase-2): 分页批量 revoke。"
SERVICE EnableRole(roleCode(STRING));RETURN {success:BOOL,message:STRING,revokedUserCount:INT}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,revokedUserCount:0}
-<VirtualTable-UserRoles "roles">.select([["roleCode","eq",roleCode]],null,[0,1],null) -> _existResult
-IF _existResult.dataArray.length == 0
--RETURN {success:false,message:"role not found",revokedUserCount:0}
-IF _existResult.dataArray[0].status == "active"
--RETURN {success:true,message:"already active",revokedUserCount:0}
-<VirtualTable-UserRoles "roles">.update([["roleCode","eq",roleCode]],[["status","set","active"]],1) -> _updateResult
-IF !_updateResult.success
--RETURN {success:false,message:_updateResult.message,revokedUserCount:0}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:true,message:("role enabled but token revoke skipped: " + _ucResult.message),revokedUserCount:0}
-<VirtualTable-UserRoleAssignments "roleAssign">.select([["roleCode","eq",roleCode],["status","eq","active"]],null,[0,1000],["userId"]) -> _affected
-_revokedUserCount(INT) = 0
-FOR (_i(INT) = 0; _i < _affected.dataArray.length; _i++)
--<TokenIssuer "tokenIssuer">.revokeAllUserTokens(_affected.dataArray[_i].userId, _ucResult.realmCode) -> _r
--IF _r.success
---_revokedUserCount = _revokedUserCount + 1
-RETURN {success:true,message:"",revokedUserCount:_revokedUserCount}

// @meta service description:"为用户分配角色。校验角色 active；若用户已有 disabled 历史记录则重新激活；成功后撤销该用户当前用户中心 token，强制重新登录。"
SERVICE AssignRoleToUser(userId(STRING),roleCode(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-<VirtualTable-UserRoles "roles">.count([["roleCode","eq",roleCode],["status","eq","active"]]) -> _roleExist
-IF _roleExist.count == 0
--RETURN {success:false,message:"role not found or not active"}
-<VirtualTable-UserRoleAssignments "roleAssign">.select([["userId","eq",userId],["roleCode","eq",roleCode]],null,[0,1],null) -> _existResult
-IF _existResult.dataArray.length > 0
--_curr({}) = _existResult.dataArray[0]
--IF _curr.status == "active"
---RETURN {success:true,message:"already assigned"}
--<VirtualTable-UserRoleAssignments "roleAssign">.update([["userId","eq",userId],["roleCode","eq",roleCode]],[["status","set","active"],["assignedBy","set",_guard.userId]],1) -> _reactivate
--IF !_reactivate.success
---RETURN {success:false,message:_reactivate.message}
--revokeUserTokensInCurrentRealm(userId) -> _rev
--RETURN {success:true,message:"reactivated"}
-<VirtualTable-UserRoleAssignments "roleAssign">.insert({userId:userId,roleCode:roleCode,assignedBy:_guard.userId,status:"active"}) -> _insertResult
-IF !_insertResult.success
--RETURN {success:false,message:_insertResult.message}
-revokeUserTokensInCurrentRealm(userId) -> _rev
-RETURN {success:true,message:""}

// @meta service description:"取消用户角色。成功后撤销该用户当前用户中心 token。"
SERVICE UnassignRoleFromUser(userId(STRING),roleCode(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-// 自锁保护：不允许移除最后一个 active admin（否则守卫回退 bootstrap 全开放）
-IF roleCode == "admin"
--<VirtualTable-UserRoleAssignments "roleAssign">.count([["roleCode","eq","admin"],["status","eq","active"]]) -> _adminCnt
--IF _adminCnt.count <= 1
---RETURN {success:false,message:"cannot remove the last active 'admin' assignment"}
-<VirtualTable-UserRoleAssignments "roleAssign">.update([["userId","eq",userId],["roleCode","eq",roleCode]],[["status","set","disabled"]],1) -> _updateResult
-IF !_updateResult.success
--RETURN {success:false,message:_updateResult.message}
-revokeUserTokensInCurrentRealm(userId) -> _rev
-RETURN {success:true,message:""}

// @meta service description:"读取某用户当前生效角色列表。口径与 buildSubjectInfo 完全一致：UserRoleAssignments(active) ∩ UserRoles(active)。避免详情页显示已停用角色。"
SERVICE GetUserRoles(userId(STRING));RETURN {success:BOOL,roles:[STRING],message:STRING}
-currentCaller() -> _cc
-IF !(_cc.isLogin && _cc.userId == userId)
--requireAdminRole() -> _guard
--IF !_guard.ok
---RETURN {success:false,roles:[],message:_guard.message}
-<VirtualTable-UserRoleAssignments "roleAssign">.select([["userId","eq",userId],["status","eq","active"]],null,[0,200],["roleCode"]) -> _listResult
-_assignedCodes([STRING]) = []
-FOR (_i(INT) = 0; _i < _listResult.dataArray.length; _i++)
--_assignedCodes.push(_listResult.dataArray[_i].roleCode)
-_codes([STRING]) = []
-IF _assignedCodes.length > 0
--<VirtualTable-UserRoles "roles">.select([["roleCode","in",_assignedCodes],["status","eq","active"]],null,[0,200],["roleCode"]) -> _activeRolesResult
--FOR (_k(INT) = 0; _k < _activeRolesResult.dataArray.length; _k++)
---_codes.push(_activeRolesResult.dataArray[_k].roleCode)
-RETURN {success:true,roles:_codes,message:""}

// @meta service description:"部门列表（管理后台）。返回全部部门，按 sort 升序、deptCode 升序排序。"
SERVICE ListDepartments();RETURN {success:BOOL,departments:[{}],message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,departments:[],message:_guard.message}
-<VirtualTable-Departments "depts">.select(null,[["sort","asc"],["deptCode","asc"]],[0,500],null) -> _listResult
-RETURN {success:true,departments:_listResult.dataArray,message:""}

// @meta service description:"创建部门节点。deptCode 必须唯一；parentDeptCode 不为空时必须存在且 active。"
SERVICE CreateDepartment(deptCode(STRING),parentDeptCode(STRING),name(STRING),sort(INT));RETURN {success:BOOL,message:STRING,deptCode:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,deptCode:""}
-IF deptCode == "" || deptCode == NULL
--RETURN {success:false,message:"deptCode required",deptCode:""}
-IF name == "" || name == NULL
--RETURN {success:false,message:"name required",deptCode:deptCode}
-<VirtualTable-Departments "depts">.count([["deptCode","eq",deptCode]]) -> _existResult
-IF _existResult.count > 0
--RETURN {success:false,message:"deptCode already exists",deptCode:deptCode}
-IF parentDeptCode != "" && parentDeptCode != NULL
--<VirtualTable-Departments "depts">.count([["deptCode","eq",parentDeptCode],["status","eq","active"]]) -> _parentExist
--IF _parentExist.count == 0
---RETURN {success:false,message:"parent department not found or not active",deptCode:deptCode}
-<VirtualTable-Departments "depts">.insert({deptCode:deptCode,parentDeptCode:parentDeptCode,name:name,status:"active",sort:sort}) -> _insertResult
-RETURN {success:_insertResult.success,message:_insertResult.message,deptCode:deptCode}

// @meta service description:"更新部门节点。deptCode 不可修改；parentDeptCode 可改动，但不得指向自身或自身后代。phase-1 不做后代环路完整检查，仅阻止直接指向自身。"
SERVICE UpdateDepartment(deptCode(STRING),parentDeptCode(STRING),name(STRING),sort(INT));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-<VirtualTable-Departments "depts">.count([["deptCode","eq",deptCode]]) -> _existResult
-IF _existResult.count == 0
--RETURN {success:false,message:"department not found"}
-IF parentDeptCode == deptCode
--RETURN {success:false,message:"parentDeptCode must not point to self"}
-IF parentDeptCode != "" && parentDeptCode != NULL
--<VirtualTable-Departments "depts">.count([["deptCode","eq",parentDeptCode],["status","eq","active"]]) -> _parentExist
--IF _parentExist.count == 0
---RETURN {success:false,message:"parent department not found or not active"}
--// 环路防护：从新父级沿 parentDeptCode 向上回溯祖先链，若经过自身=把上级改成了自身后代→成环；深度上限 200（异常数据不死循环）；全程 IF/ELSE 无 CONTINUE/BREAK
--_cyCursor(STRING) = parentDeptCode
--_cyFound(BOOL) = false
--_cyWalk(BOOL) = true
--FOR (_cyH(INT) = 0; _cyH < 200; _cyH++)
---IF _cyWalk
----IF _cyCursor == deptCode
-----_cyFound = true
-----_cyWalk = false
----ELSE
-----<VirtualTable-Departments "depts">.select([["deptCode","eq",_cyCursor]],null,[0,1],null) -> _cyRow
-----IF _cyRow.dataArray.length == 0
------_cyWalk = false
-----ELSE
------_cyNext(STRING) = _cyRow.dataArray[0].parentDeptCode ? ("" + _cyRow.dataArray[0].parentDeptCode) : ""
------IF _cyNext == ""
-------_cyWalk = false
------ELSE
-------_cyCursor = _cyNext
--IF _cyFound
---RETURN {success:false,message:"不能把上级部门设为该部门自身或其下级部门（会形成环路）"}
--IF _cyWalk
---RETURN {success:false,message:"部门层级异常或已存在环路（超过最大深度），请先修正部门树后再调整上级"}
-<VirtualTable-Departments "depts">.update([["deptCode","eq",deptCode]],[["name","set",name],["parentDeptCode","set",parentDeptCode],["sort","set",sort]],1) -> _updateResult
-RETURN {success:_updateResult.success,message:_updateResult.message}

// @meta service description:"停用部门节点。停用后部门目录置 disabled；UserDepartments 历史记录不动，调用 EnableDepartment 可直接恢复；子部门不级联停用，phase-1 由 UI 提示管理员人工处理。
// 立即撤销当前用户中心下所有归属该部门的用户 token，让旧 token 不再携带已停用部门。
// phase-1 上限一次最多 revoke 1000 个用户；超过 1000 的尾巴依靠 buildSubjectInfo 在下次登录时过滤掉，
// 安全降级但有 token 延迟。TODO(phase-2): 分页批量 revoke，覆盖大客户场景。"
SERVICE DisableDepartment(deptCode(STRING));RETURN {success:BOOL,message:STRING,revokedUserCount:INT}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,revokedUserCount:0}
-<VirtualTable-Departments "depts">.count([["deptCode","eq",deptCode]]) -> _existResult
-IF _existResult.count == 0
--RETURN {success:false,message:"department not found",revokedUserCount:0}
-<VirtualTable-Departments "depts">.update([["deptCode","eq",deptCode]],[["status","set","disabled"]],1) -> _updateResult
-IF !_updateResult.success
--RETURN {success:false,message:_updateResult.message,revokedUserCount:0}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:true,message:("department disabled but token revoke skipped: " + _ucResult.message),revokedUserCount:0}
-<VirtualTable-UserDepartments "userDepts">.select([["deptCode","eq",deptCode],["status","eq","active"]],null,[0,1000],["userId"]) -> _affected
-_revokedUserCount(INT) = 0
-FOR (_i(INT) = 0; _i < _affected.dataArray.length; _i++)
--<TokenIssuer "tokenIssuer">.revokeAllUserTokens(_affected.dataArray[_i].userId, _ucResult.realmCode) -> _r
--IF _r.success
---_revokedUserCount = _revokedUserCount + 1
-RETURN {success:true,message:"",revokedUserCount:_revokedUserCount}

// @meta service description:"恢复部门节点为 active。会撤销所有归属该部门的活跃 UserDepartments 用户 token，让下次登录的 subjectInfo 重新包含该部门。
// phase-1 revoke 上限 1000 同 DisableDepartment。TODO(phase-2): 分页批量 revoke。
// 注意：父部门若处于 disabled，本服务不会自动启用父部门；子部门若已被独立停用，本服务也不会级联恢复。"
SERVICE EnableDepartment(deptCode(STRING));RETURN {success:BOOL,message:STRING,revokedUserCount:INT}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,revokedUserCount:0}
-<VirtualTable-Departments "depts">.select([["deptCode","eq",deptCode]],null,[0,1],null) -> _existResult
-IF _existResult.dataArray.length == 0
--RETURN {success:false,message:"department not found",revokedUserCount:0}
-IF _existResult.dataArray[0].status == "active"
--RETURN {success:true,message:"already active",revokedUserCount:0}
-<VirtualTable-Departments "depts">.update([["deptCode","eq",deptCode]],[["status","set","active"]],1) -> _updateResult
-IF !_updateResult.success
--RETURN {success:false,message:_updateResult.message,revokedUserCount:0}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:true,message:("department enabled but token revoke skipped: " + _ucResult.message),revokedUserCount:0}
-<VirtualTable-UserDepartments "userDepts">.select([["deptCode","eq",deptCode],["status","eq","active"]],null,[0,1000],["userId"]) -> _affected
-_revokedUserCount(INT) = 0
-FOR (_i(INT) = 0; _i < _affected.dataArray.length; _i++)
--<TokenIssuer "tokenIssuer">.revokeAllUserTokens(_affected.dataArray[_i].userId, _ucResult.realmCode) -> _r
--IF _r.success
---_revokedUserCount = _revokedUserCount + 1
-RETURN {success:true,message:"",revokedUserCount:_revokedUserCount}

// @meta service description:"为用户分配部门。isPrimary=true 时自动清空该用户其他部门的主标记。成功后撤销该用户当前用户中心 token。"
SERVICE AssignDepartmentToUser(userId(STRING),deptCode(STRING),isPrimary(BOOL));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-<VirtualTable-Departments "depts">.count([["deptCode","eq",deptCode],["status","eq","active"]]) -> _deptExist
-IF _deptExist.count == 0
--RETURN {success:false,message:"department not found or not active"}
-IF isPrimary
--<VirtualTable-UserDepartments "userDepts">.update([["userId","eq",userId],["isPrimary","eq",true]],[["isPrimary","set",false]],50) -> _clearPrimary
-<VirtualTable-UserDepartments "userDepts">.count([["userId","eq",userId],["deptCode","eq",deptCode]]) -> _existResult
-IF _existResult.count > 0
--<VirtualTable-UserDepartments "userDepts">.update([["userId","eq",userId],["deptCode","eq",deptCode]],[["status","set","active"],["isPrimary","set",isPrimary]],1) -> _updateResult
--IF !_updateResult.success
---RETURN {success:false,message:_updateResult.message}
--revokeUserTokensInCurrentRealm(userId) -> _rev
--RETURN {success:true,message:""}
-<VirtualTable-UserDepartments "userDepts">.insert({userId:userId,deptCode:deptCode,isPrimary:isPrimary,assignedBy:_guard.userId,status:"active"}) -> _insertResult
-IF !_insertResult.success
--RETURN {success:false,message:_insertResult.message}
-revokeUserTokensInCurrentRealm(userId) -> _rev
-RETURN {success:true,message:""}

// @meta service description:"移除用户部门归属（标记 status=disabled，保留历史记录）。成功后撤销该用户当前用户中心 token。"
SERVICE UnassignDepartmentFromUser(userId(STRING),deptCode(STRING));RETURN {success:BOOL,message:STRING}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message}
-<VirtualTable-UserDepartments "userDepts">.update([["userId","eq",userId],["deptCode","eq",deptCode]],[["status","set","disabled"],["isPrimary","set",false]],1) -> _updateResult
-IF !_updateResult.success
--RETURN {success:false,message:_updateResult.message}
-revokeUserTokensInCurrentRealm(userId) -> _rev
-RETURN {success:true,message:""}

// @meta service description:"读取某用户当前生效部门列表。口径与 buildSubjectInfo 完全一致：UserDepartments(active) ∩ Departments(active)。
// 若主部门已停用，primaryDepartmentCode 会随之消失（即使 UserDepartments.isPrimary 仍为 true）。"
SERVICE GetUserDepartments(userId(STRING));RETURN {success:BOOL,departments:[{}],primaryDepartmentCode:STRING,message:STRING}
-currentCaller() -> _cc
-IF !(_cc.isLogin && _cc.userId == userId)
--requireAdminRole() -> _guard
--IF !_guard.ok
---RETURN {success:false,departments:[],primaryDepartmentCode:"",message:_guard.message}
-<VirtualTable-UserDepartments "userDepts">.select([["userId","eq",userId],["status","eq","active"]],null,[0,50],null) -> _listResult
-_assignedDeptCodes([STRING]) = []
-FOR (_i(INT) = 0; _i < _listResult.dataArray.length; _i++)
--_assignedDeptCodes.push(_listResult.dataArray[_i].deptCode)
-_activeDeptCatalog([STRING]) = []
-IF _assignedDeptCodes.length > 0
--<VirtualTable-Departments "depts">.select([["deptCode","in",_assignedDeptCodes],["status","eq","active"]],null,[0,50],["deptCode"]) -> _activeDeptsResult
--FOR (_m(INT) = 0; _m < _activeDeptsResult.dataArray.length; _m++)
---_activeDeptCatalog.push(_activeDeptsResult.dataArray[_m].deptCode)
-_effective([{}]) = []
-_primary(STRING) = ""
-FOR (_n(INT) = 0; _n < _listResult.dataArray.length; _n++)
--IF _activeDeptCatalog.indexOf(_listResult.dataArray[_n].deptCode) >= 0
---_effective.push(_listResult.dataArray[_n])
---IF _listResult.dataArray[_n].isPrimary
----_primary = _listResult.dataArray[_n].deptCode
-RETURN {success:true,departments:_effective,primaryDepartmentCode:_primary,message:""}

// @meta service description:"内部：用户中心基础信息读取。仅供用户中心自身页面调用。B 组（权限中心）通过 bindOnly 直接读 UserCenterConfig 表，不再走此 SERVICE。userCenterNid 来自 SYSENV.currentApp.nid。"
SERVICE GetUserCenterInfo(env(STRING),userSpaceCode(STRING));RETURN {success:BOOL,message:STRING,userCenterNid:STRING,userSpaceCode:STRING,displayName:STRING,status:STRING,roleCatalogVersion:STRING,departmentTreeVersion:STRING,subjectSchemaVersion:STRING,updatedAt:INT}
-loadUserCenterInfo(env, userSpaceCode) -> _r
-RETURN _r

// @meta service description:"内部：角色目录读取。B 组通过 bindOnly 直接读 UserRoles 表，不再走此 SERVICE。"
SERVICE GetRoleCatalog(env(STRING),userSpaceCode(STRING));RETURN {success:BOOL,message:STRING,roles:[{}]}
-loadRoleCatalog(env, userSpaceCode) -> _r
-RETURN _r

// @meta service description:"内部：部门树扁平节点列表读取。B 组通过 bindOnly 直接读 Departments 表，不再走此 SERVICE。"
SERVICE GetDepartmentTree(env(STRING),userSpaceCode(STRING));RETURN {success:BOOL,message:STRING,departments:[{}]}
-loadDepartmentTree(env, userSpaceCode) -> _r
-RETURN _r

// @meta service description:"OAuthBundle 占位服务。第一阶段不实现第三方登录。"
SERVICE CompleteOAuthLogin(providerCode(STRING),code(STRING),state(STRING));RETURN {success:BOOL,message:STRING}
-RETURN {success:false,message:"oauth provider not enabled in phase 1"}

// @meta service description:"当前用户更新自己的基础资料。包装 UserStore.updateBasicProfile + TokenIssuer.refreshUserInfo，避免前端再单独触发 token 刷新。"
SERVICE UpdateMyProfile(nickname(STRING),displayName(STRING),avatar(STRING));RETURN {success:BOOL,message:STRING,user:{}}
-currentCaller() -> _cc
-IF !_cc.isLogin
--RETURN {success:false,message:"not logged in",user:{}}
-_uid(STRING) = _cc.userId
-<UserStore "userStore">.updateBasicProfile(_uid, nickname, displayName, avatar, NULL) -> _upR
-IF !_upR.success
--RETURN {success:false,message:_upR.message,user:{}}
-resolveUserCenterNid() -> _ucResult
-IF _ucResult.success
--<TokenIssuer "tokenIssuer">.refreshUserInfo(_uid, _ucResult.realmCode, _upR.user) -> _refR
-RETURN {success:true,message:"",user:_upR.user}

// @meta service description:"当前用户的会话列表（多设备登录态）。包装 TokenIssuer.getUserSessions。"
SERVICE ListMySessions();RETURN {success:BOOL,message:STRING,sessions:[{}]}
-currentCaller() -> _cc
-IF !_cc.isLogin
--RETURN {success:false,message:"not logged in",sessions:[]}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,sessions:[]}
-<TokenIssuer "tokenIssuer">.getUserSessions(_cc.userId, _ucResult.realmCode) -> _sR
-RETURN {success:_sR.success,message:"",sessions:_sR.sessions}

// @meta service description:"撤销自己名下的某个会话 token（其他设备）。包装 TokenIssuer.revokeToken。"
SERVICE RevokeMySession(tokenId(STRING));RETURN {success:BOOL,message:STRING}
-currentCaller() -> _cc
-IF !_cc.isLogin
--RETURN {success:false,message:"not logged in"}
-<TokenIssuer "tokenIssuer">.revokeToken(tokenId) -> _r
-RETURN {success:_r.success,message:_r.message}

// @meta service description:"撤销自己名下除当前会话外所有 token，等价于"踢出所有其他会话"。"
SERVICE RevokeAllOtherSessions();RETURN {success:BOOL,message:STRING,revokedCount:INT}
-currentCaller() -> _cc
-IF !_cc.isLogin
--RETURN {success:false,message:"not logged in",revokedCount:0}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,revokedCount:0}
-<TokenIssuer "tokenIssuer">.getCurrentSession(_ucResult.realmCode) -> _curR
-_curTokenId(STRING) = (_curR.success && _curR.session) ? _curR.session.tokenId : ""
-<TokenIssuer "tokenIssuer">.getUserSessions(_cc.userId, _ucResult.realmCode) -> _sR
-_kicked(INT) = 0
-IF _sR.success
--FOR (_i(INT) = 0; _i < _sR.sessions.length; _i++)
---_tid(STRING) = _sR.sessions[_i].tokenId
---IF _tid != _curTokenId && _tid != ""
----<TokenIssuer "tokenIssuer">.revokeToken(_tid) -> _rev
----IF _rev.success
-----_kicked = _kicked + 1
-RETURN {success:true,message:"",revokedCount:_kicked}

// @meta service description:"登录审计列表（admin）。支持按 userId / eventType / result / 关键字过滤；按时间倒序。"
SERVICE ListLoginAudit(userId(STRING),eventType(STRING),result(STRING),keyword(STRING),offset(INT),limit(INT));RETURN {success:BOOL,message:STRING,total:INT,entries:[{}]}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,message:_guard.message,total:0,entries:[]}
-_safeLimit(INT) = (limit > 0 && limit <= 200) ? limit : 50
-_safeOffset(INT) = offset > 0 ? offset : 0
-_filters([{}]) = []
-IF userId != "" && userId != NULL
--_filters.push(["userId","eq",userId])
-IF eventType != "" && eventType != NULL
--_filters.push(["eventType","eq",eventType])
-IF result != "" && result != NULL
--_filters.push(["result","eq",result])
-IF keyword != "" && keyword != NULL
--_filters.push(["accountInput","like",keyword])
-IF _filters.length == 0
--<VirtualTable-LoginAuditLogs "audit">.count(null) -> _cntR
--<VirtualTable-LoginAuditLogs "audit">.select(null,[["_create","desc"]],[_safeOffset,_safeLimit],null) -> _listR
--RETURN {success:true,message:"",total:_cntR.count,entries:_listR.dataArray}
-<VirtualTable-LoginAuditLogs "audit">.count(_filters) -> _cntR
-<VirtualTable-LoginAuditLogs "audit">.select(_filters,[["_create","desc"]],[_safeOffset,_safeLimit],null) -> _listR
-RETURN {success:true,message:"",total:_cntR.count,entries:_listR.dataArray}

// @meta service description:"管理概览统计。一次给出用户/角色/部门各项总数和待审核数，避免前端打多个 count 请求。"
SERVICE GetAdminStats();RETURN {success:BOOL,userTotal:INT,userActive:INT,userPendingApproval:INT,userDisabled:INT,roleTotal:INT,roleActive:INT,deptTotal:INT,deptActive:INT}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,userTotal:0,userActive:0,userPendingApproval:0,userDisabled:0,roleTotal:0,roleActive:0,deptTotal:0,deptActive:0}
-<VirtualTable-AuthUsers "authUsers">.count(null) -> _userTotal
-<VirtualTable-AuthUsers "authUsers">.count([["status","eq","active"]]) -> _userActive
-<VirtualTable-AuthUsers "authUsers">.count([["status","eq","pendingApproval"]]) -> _userPending
-<VirtualTable-AuthUsers "authUsers">.count([["status","eq","disabled"]]) -> _userDisabled
-<VirtualTable-UserRoles "roles">.count(null) -> _roleTotal
-<VirtualTable-UserRoles "roles">.count([["status","eq","active"]]) -> _roleActive
-<VirtualTable-Departments "depts">.count(null) -> _deptTotal
-<VirtualTable-Departments "depts">.count([["status","eq","active"]]) -> _deptActive
-RETURN {success:true,userTotal:_userTotal.count,userActive:_userActive.count,userPendingApproval:_userPending.count,userDisabled:_userDisabled.count,roleTotal:_roleTotal.count,roleActive:_roleActive.count,deptTotal:_deptTotal.count,deptActive:_deptActive.count}

// @meta service description:"管理概览最近注册（按 _create 倒序前 N 条）。"
SERVICE ListRecentUsers(limit(INT));RETURN {success:BOOL,users:[{}]}
-requireAdminRole() -> _guard
-IF !_guard.ok
--RETURN {success:false,users:[]}
-_safe(INT) = (limit > 0 && limit <= 20) ? limit : 5
-<VirtualTable-AuthUsers "authUsers">.select(null,[["_create","desc"]],[0,_safe],null) -> _r
-RETURN {success:true,users:_r.dataArray}

# Backend Event Handlers

# Transactions

# Backend Internal Methods

// A1 诊断/绕障：登录全流程下沉为 internal METHOD（已验证 internal METHOD 在 PUBLIC 上下文可用），
// SERVICE LoginByPassword 与 PUBLIC_SERVICE PasswordLogin 都 delegate 到这里，逻辑单份。
METHOD loginByPasswordCore(account(STRING),password(STRING));RETURN {success:BOOL,message:STRING,userId:STRING,tokenId:STRING,expiresAt:INT,user:{},subjectInfo:{}}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{}}
-_userCenterNid(STRING) = _ucResult.nid
-_realmCode(STRING) = _ucResult.realmCode
-_sessionDuration(INT) = _ucResult.sessionDuration
-_maxConcurrent(INT) = _ucResult.maxConcurrent
-// UC-008A：账号密码登录开关（登录配置可编辑后必须在登录链路生效）
-IF !_ucResult.passwordLoginOpen
--writeAudit("", account, "password", "login", "failure", "password_login_disabled") -> _aOff
--RETURN {success:false,message:"password login is disabled",userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{}}
-<UserStore "userStore">.verifyUserByPassword(account, password) -> _loginResult
-IF !_loginResult.success
--writeAudit("", account, "password", "login", "failure", _loginResult.message) -> _a
--RETURN {success:false,message:"authentication failed",userId:"",tokenId:"",expiresAt:0,user:{},subjectInfo:{}}
-_userIdStr(STRING) = "" + _loginResult.userId
-// UC-001：extgo verifyUserByPassword 的 user/subjectInfo 不含可靠 status（user 无 status，
-// subjectInfo 是 accountStatus 且 buildSubjectInfo 处恒为 "active"）。改读 auth_users 权威状态列，
-// 即 DisableUser / 审批流真正写入的字段；只拦显式 disabled / pendingApproval，缺省放行。
-_userIdInt(INT) = _userIdStr * 1
-<VirtualTable-AuthUsers "authUsers">.select([["_id","eq",_userIdInt]],null,[0,1],null) -> _statusRow
-_status(STRING) = (_statusRow.dataArray.length > 0 && _statusRow.dataArray[0].status) ? ("" + _statusRow.dataArray[0].status) : ""
-IF _status == "disabled" || _status == "pendingApproval"
--writeAudit(_userIdStr, account, "password", "login", "failure", ("account_" + _status)) -> _a
--RETURN {success:false,message:("account " + _status),userId:_userIdStr,tokenId:"",expiresAt:0,user:_loginResult.user,subjectInfo:{}}
-buildSubjectInfo(_userIdStr, _userCenterNid, _loginResult.subjectInfo, _status) -> _subj
-<TokenIssuer "tokenIssuer">.generateLoginToken(_userIdStr, _realmCode, _subj.subjectInfo, _loginResult.user, _maxConcurrent, _sessionDuration) -> _tokenResult
-IF !_tokenResult.success
--writeAudit(_userIdStr, account, "password", "login", "failure", _tokenResult.message) -> _a
--RETURN {success:false,message:_tokenResult.message,userId:_userIdStr,tokenId:"",expiresAt:0,user:_loginResult.user,subjectInfo:_subj.subjectInfo}
-writeAudit(_userIdStr, account, "password", "login", "success", "") -> _a
-RETURN {success:true,message:"",userId:_userIdStr,tokenId:_tokenResult.tokenId,expiresAt:_tokenResult.expiresAt,user:_loginResult.user,subjectInfo:_subj.subjectInfo}

// A1 + UC-MERGE-008：前端跳转 URL 统一出口。AB 合并后身份域与权限域同一前端/同一后端：
// 正式来源仍是 UserCenterConfig 行（userCenterUrl / permissionCenterUrl）；未配置时
// permissionCenter 一律回落到合并项目自身（pcNid = SYSENV.currentApp.nid，pcUrl = ucUrl），
// 不再指向已废弃的独立 B 部署；模板内不得固化任何具体部署 nid 或前端短码。
// ucUrl fallback 必须来自实例配置；缺失时返回 fail-closed，不再回落到旧预览产物。
METHOD resolveFrontendUrls();RETURN {userCenterUrl:STRING,permissionCenterUrl:STRING,permissionCenterBackendNid:INT}
-_ucUrl(STRING) = ""
-_pcUrl(STRING) = ""
-_pcNid(INT) = 0
-// UC-A-CONFIG-20260616-001：正式配置源（优先）=默认用户中心实例（UserCenterRegistry），
-// 由管理员在「实例与绑定」维护 userCenterFrontendUrl；不写死预览短码。
-<VirtualTable-UserCenterRegistry "ucRegistry">.select([["isDefault","eq",true],["status","eq","active"]],[["_id","asc"]],[0,1],null) -> _regDef
-IF _regDef.dataArray.length > 0
--_rd({}) = _regDef.dataArray[0]
--_ucUrl = _rd.userCenterFrontendUrl ? ("" + _rd.userCenterFrontendUrl) : ""
--_pcUrl = _rd.permissionCenterFrontendUrl ? ("" + _rd.permissionCenterFrontendUrl) : ""
--_pcNid = _rd.permissionCenterBackendNid ? _rd.permissionCenterBackendNid : 0
-// 兼容旧 UserCenterConfig.userCenterUrl（仅在实例未配置时回落）
-IF _ucUrl == ""
--<VirtualTable-UserCenterConfig "config">.select([["status","eq","active"]],[["_id","asc"]],[0,1],null) -> _cfgResult
--IF _cfgResult.dataArray.length > 0
---_cfg({}) = _cfgResult.dataArray[0]
---_ucUrl = _cfg.userCenterUrl ? ("" + _cfg.userCenterUrl) : ""
---IF _pcUrl == ""
----_pcUrl = _cfg.permissionCenterUrl ? ("" + _cfg.permissionCenterUrl) : ""
---IF _pcNid == 0
----_pcNid = _cfg.permissionCenterBackendNid ? _cfg.permissionCenterBackendNid : 0
-// 本地 system service 允许从 SYSENV.currentApp 合成 127.0.0.1/localhost play URL；云端仍 fail-closed，不回落旧短码。
-_rfHost(STRING) = SYSENV.currentApp.previewDomain
-IF _ucUrl == "" && (_rfHost.indexOf("127.0.0.1") == 0 || _rfHost.indexOf("localhost") == 0)
--_ucUrl = "http://" + _rfHost + SYSENV.currentApp.playPath
-// 未配置时返回空 URL（不静默跳旧环境）；由 GetClientConfig 显式报“登录入口未配置”
-IF _pcUrl == ""
--_pcUrl = _ucUrl
-IF _pcNid == 0
--_pcNid = SYSENV.currentApp.nid
-RETURN {userCenterUrl:_ucUrl,permissionCenterUrl:_pcUrl,permissionCenterBackendNid:_pcNid}

// UC-007A：多用户中心实例解析。解析顺序固定：
//   1) ProjectUserCenterBindings（中心化绑定，最高优先，项目配置不得覆盖） -> bindingSource="projectBinding"
//   2) 项目配置 configuredNid（项目自己配的 userPermissionCenterNid）        -> bindingSource="projectConfig"
//   3) 默认用户中心（UserCenterRegistry.isDefault=true）                      -> bindingSource="defaultUserCenter"
// Registry 无可用行时合成"当前实例"（SYSENV.currentApp.nid + resolveFrontendUrls），保证未播种前不破链。
METHOD resolveUserCenterInstance(projectId(STRING),env(STRING),configuredNid(INT));RETURN {found:BOOL,bindingSource:STRING,userCenterCode:STRING,displayName:STRING,ucNid:INT,ucFrontendUrl:STRING,pcNid:INT,pcFrontendUrl:STRING}
-_envValue(STRING) = (env != "" && env != NULL) ? env : "preview"
-// 1) 中心化绑定
-IF projectId != "" && projectId != NULL
--<VirtualTable-ProjectUserCenterBindings "projectBindings">.select([["projectId","eq",projectId],["env","eq",_envValue],["status","eq","active"]],[["_id","asc"]],[0,1],null) -> _bindResult
--IF _bindResult.dataArray.length > 0
---_bindCode(STRING) = _bindResult.dataArray[0].userCenterCode
---<VirtualTable-UserCenterRegistry "ucRegistry">.select([["userCenterCode","eq",_bindCode],["status","eq","active"]],[["_id","asc"]],[0,1],null) -> _regByCode
---IF _regByCode.dataArray.length > 0
----_r1({}) = _regByCode.dataArray[0]
----RETURN {found:true,bindingSource:"projectBinding",userCenterCode:_r1.userCenterCode,displayName:("" + _r1.displayName),ucNid:_r1.userCenterBackendNid,ucFrontendUrl:("" + _r1.userCenterFrontendUrl),pcNid:(_r1.permissionCenterBackendNid ? _r1.permissionCenterBackendNid : 0),pcFrontendUrl:("" + _r1.permissionCenterFrontendUrl)}
-// 2) 项目配置 nid
-IF configuredNid > 0
--<VirtualTable-UserCenterRegistry "ucRegistry">.select([["userCenterBackendNid","eq",configuredNid],["status","eq","active"]],[["_id","asc"]],[0,1],null) -> _regByNid
--IF _regByNid.dataArray.length > 0
---_r2({}) = _regByNid.dataArray[0]
---RETURN {found:true,bindingSource:"projectConfig",userCenterCode:_r2.userCenterCode,displayName:("" + _r2.displayName),ucNid:_r2.userCenterBackendNid,ucFrontendUrl:("" + _r2.userCenterFrontendUrl),pcNid:(_r2.permissionCenterBackendNid ? _r2.permissionCenterBackendNid : 0),pcFrontendUrl:("" + _r2.permissionCenterFrontendUrl)}
--IF configuredNid == SYSENV.currentApp.nid
---resolveFrontendUrls() -> _selfUrls2
---RETURN {found:true,bindingSource:"projectConfig",userCenterCode:"current",displayName:"",ucNid:SYSENV.currentApp.nid,ucFrontendUrl:_selfUrls2.userCenterUrl,pcNid:_selfUrls2.permissionCenterBackendNid,pcFrontendUrl:_selfUrls2.permissionCenterUrl}
-// 3) 默认用户中心
-<VirtualTable-UserCenterRegistry "ucRegistry">.select([["isDefault","eq",true],["status","eq","active"]],[["_id","asc"]],[0,1],null) -> _regDefault
-IF _regDefault.dataArray.length > 0
--_r3({}) = _regDefault.dataArray[0]
--RETURN {found:true,bindingSource:"defaultUserCenter",userCenterCode:_r3.userCenterCode,displayName:("" + _r3.displayName),ucNid:_r3.userCenterBackendNid,ucFrontendUrl:("" + _r3.userCenterFrontendUrl),pcNid:(_r3.permissionCenterBackendNid ? _r3.permissionCenterBackendNid : 0),pcFrontendUrl:("" + _r3.permissionCenterFrontendUrl)}
-resolveFrontendUrls() -> _selfUrls3
-RETURN {found:true,bindingSource:"defaultUserCenter",userCenterCode:"current",displayName:"",ucNid:SYSENV.currentApp.nid,ucFrontendUrl:_selfUrls3.userCenterUrl,pcNid:_selfUrls3.permissionCenterBackendNid,pcFrontendUrl:_selfUrls3.permissionCenterUrl}

// UC-008A（版本17 收紧）：管理操作守卫。要求当前登录用户持有 A 侧 active "admin" 角色
// （UserRoleAssignments(active) ∩ UserRoles(active)，与 buildSubjectInfo 同口径）。
// 覆盖范围：全部管理读写服务（用户/角色/部门 CRUD、审批、审计、统计、登录配置、OAuth、
// 实例/绑定管理、CSV 导入）；自助服务（注册/登录/登出/我的资料/我的会话）不守卫；
// UC-A-PRODUCT-20260615-006 平台墙：多用户共用同一 realm("default") 时，平台 SYSENV.currentUser
// 解析不稳定——重新登录的非首用户会被错误解析回 realm 首个用户（实测 uid=2 token 解析成 uid=1），
// 导致管理员重登后导航消失、注册即登录身份串号。TokenIssuer.getCurrentSession 按实际呈递 token 稳定
// 解析（实测 setup/register/login 三场景全部正确）。统一改走 currentCaller()；平台修复 SYSENV.currentUser
// 后可整体回退到 SYSENV.currentUser。
METHOD currentCaller();RETURN {isLogin:BOOL,userId:STRING,userInfo:{}}
-resolveUserCenterNid() -> _ucc
-<TokenIssuer "tokenIssuer">.getCurrentSession(_ucc.realmCode) -> _sess
-IF !_sess.success || !_sess.session || !_sess.session.userInfo
--RETURN {isLogin:false,userId:"",userInfo:{}}
-_ccUid(STRING) = "" + _sess.session.userInfo.id
-IF _ccUid == "" || _ccUid == "0" || _ccUid == "null"
--RETURN {isLogin:false,userId:"",userInfo:{}}
-RETURN {isLogin:true,userId:_ccUid,userInfo:_sess.session.userInfo}

// GetUserById/GetUserRoles/GetUserDepartments 为 self-or-admin。
// Bootstrap 例外：系统尚无任何 active admin 角色持有者时，放行任何【已登录】用户，
// 供首个管理员自举（注册->登录->给自己分配 admin）；一旦存在 admin 持有者立即完全收紧。
// 配套自锁保护：DisableRole 禁止停用 "admin"；UnassignRoleFromUser 禁止移除最后一个 admin。
// 注意这是 A 域内"身份角色"判定，不是 B 的管理员授权体系（那是 007B 的事）。
METHOD requireAdminRole();RETURN {ok:BOOL,userId:STRING,message:STRING}
-currentCaller() -> _cc
-IF !_cc.isLogin
--RETURN {ok:false,userId:"",message:"not logged in"}
-_uid(STRING) = _cc.userId
-<VirtualTable-UserRoleAssignments "roleAssign">.count([["roleCode","eq","admin"],["status","eq","active"]]) -> _anyAdmin
-IF _anyAdmin.count == 0
--RETURN {ok:true,userId:_uid,message:"bootstrap mode: no admin assigned yet"}
-<VirtualTable-UserRoleAssignments "roleAssign">.count([["userId","eq",_uid],["roleCode","eq","admin"],["status","eq","active"]]) -> _asg
-IF _asg.count == 0
--RETURN {ok:false,userId:_uid,message:"requires active 'admin' role"}
-<VirtualTable-UserRoles "roles">.count([["roleCode","eq","admin"],["status","eq","active"]]) -> _cat
-IF _cat.count == 0
--RETURN {ok:false,userId:_uid,message:"role 'admin' is not active in role catalog"}
-RETURN {ok:true,userId:_uid,message:""}

// v0.2: 身份域不再来自 UserCenterConfig 行，而是固定从 VL 当前后台应用读：
//   nid          = SYSENV.currentApp.nid（INT → STRING）
//   env          = "preview"（phase-1 硬编码；phase-2 应从 SYSENV 或请求上下文检测）
//   userSpaceCode= "default"（phase-1 固定单空间）
// UserCenterConfig 行只承载设置项（displayName / registrationOpen / requireApproval / defaultRoleCode /
// sessionDuration / maxConcurrent / status），不再作为身份配置。无行时使用默认值，不影响 realm 推导。
//
// realmCode = "default"（UC-PM-20260613-001 定案，2026-06-13 线上实测取证）：
//   旧值 `nid:env:userSpaceCode`（如 "12206000:preview:default"）导致登录态永远无法落地——
//   平台 generateLoginToken 会把 token 写入名为 `ivx_a_realm_<realmCode>` 的 Set-Cookie；
//   实验1：登录响应 Set-Cookie 名= ivx_a_realm_12206000:preview:default（含冒号，违反 RFC 6265
//          cookie-name，curl/标准客户端直接拒存，cookie jar 为空）；
//   实验3a：把该 cookie 原样手动带回 -> currentuser 仍 isLogin:false（管道不读该名字）；
//   实验3b：同一 token 换 cookie 名为 ivx_a_realm_default -> isLogin:true, userId:"2" ✅；
//   即平台请求管道解析 SYSENV.currentUser 只读 `ivx_a_realm_default`。
//   因此 A 全链路（签发/getCurrentSession/revoke*/refreshUserInfo）统一用 "default"。
//   注意：B 组 resolveCaller 原按 `A_nid:preview:default` 拼 realm，必须同步改 "default"。
//   多实例 realm 隔离记为 phase-2 课题（需平台修 cookie 命名或提供合法 realm->cookie 映射）。
METHOD resolveUserCenterNid();RETURN {success:BOOL,message:STRING,nid:STRING,env:STRING,userSpaceCode:STRING,realmCode:STRING,displayName:STRING,registrationOpen:BOOL,requireApproval:BOOL,passwordLoginOpen:BOOL,defaultRoleCode:STRING,sessionDuration:INT,maxConcurrent:INT,status:STRING,updatedAt:INT}
-_backendNid(INT) = SYSENV.currentApp.nid
-IF _backendNid == 0
--RETURN {success:false,message:"SYSENV.currentApp.nid is unavailable; cannot derive realmCode",nid:"",env:"",userSpaceCode:"",realmCode:"",displayName:"",registrationOpen:false,requireApproval:false,passwordLoginOpen:false,defaultRoleCode:"",sessionDuration:0,maxConcurrent:0,status:"",updatedAt:0}
-_nidStr(STRING) = "" + _backendNid
-_envValue(STRING) = "preview"
-_userSpaceCode(STRING) = "default"
-_realm(STRING) = "default"
-<VirtualTable-UserCenterConfig "config">.select(null,[["_id","asc"]],[0,1],null) -> _cfgResult
-IF !_cfgResult.success || _cfgResult.dataArray.length == 0
--RETURN {success:true,message:"using defaults (no UserCenterConfig row)",nid:_nidStr,env:_envValue,userSpaceCode:_userSpaceCode,realmCode:_realm,displayName:"",registrationOpen:true,requireApproval:false,passwordLoginOpen:true,defaultRoleCode:"",sessionDuration:7200,maxConcurrent:5,status:"active",updatedAt:0}
-_cfg({}) = _cfgResult.dataArray[0]
-IF _cfg.status != "active"
--RETURN {success:false,message:"user center disabled; call EnsureUserCenterConfig to reactivate",nid:_nidStr,env:_envValue,userSpaceCode:_userSpaceCode,realmCode:_realm,displayName:(_cfg.displayName ? _cfg.displayName : ""),registrationOpen:false,requireApproval:false,passwordLoginOpen:false,defaultRoleCode:"",sessionDuration:0,maxConcurrent:0,status:_cfg.status,updatedAt:_cfg._update}
-// 半配置行容错（UC-PM-002 根因：旧 active 行 registrationOpen/sessionDuration/maxConcurrent 为 null，
-// 导致 registration 被误判关闭、token 立即过期）。未显式配置时回落到与"无行"分支一致的 phase-1 默认：
-// 注册默认开放，只有显式 false 才关闭；时长/并发为空回落 7200 / 5。
-_regOpen(BOOL) = _cfg.registrationOpen == false ? false : true
-_reqApproval(BOOL) = _cfg.requireApproval == true ? true : false
-_sessDur(INT) = (_cfg.sessionDuration && _cfg.sessionDuration > 0) ? _cfg.sessionDuration : 7200
-_maxConc(INT) = (_cfg.maxConcurrent && _cfg.maxConcurrent > 0) ? _cfg.maxConcurrent : 5
-// UC-008A：账号密码登录开关存于 passwordPolicyJson.passwordLoginOpen（不加新列，免去 B bindOnly 声明同步）；
-// 缺省/null = 开放，仅显式 false 关闭。
-_pwdOpen(BOOL) = (_cfg.passwordPolicyJson && _cfg.passwordPolicyJson.passwordLoginOpen == false) ? false : true
-RETURN {success:true,message:"",nid:_nidStr,env:_envValue,userSpaceCode:_userSpaceCode,realmCode:_realm,displayName:(_cfg.displayName ? _cfg.displayName : ""),registrationOpen:_regOpen,requireApproval:_reqApproval,passwordLoginOpen:_pwdOpen,defaultRoleCode:(_cfg.defaultRoleCode ? _cfg.defaultRoleCode : ""),sessionDuration:_sessDur,maxConcurrent:_maxConc,status:_cfg.status,updatedAt:_cfg._update}

// phase-1 baseline subjectInfo：只组合 UserStore 系统字段 + UserRoleAssignments + UserDepartments。
// 角色字段最终来源是 UserRoleAssignments(active) ∩ UserRoles(active)，
// 单纯 UserRoleAssignments active 不够：DisableRole 后已签 assignment 仍是 active，
// 必须再用角色目录过滤掉 disabled / deprecated 角色，才能保证 subjectInfo.roles 与运行时鉴权一致。
// SubjectFields.inSubjectInfo / SubjectDictionaries 的自定义字段在 phase-2 引入。
METHOD buildSubjectInfo(userIdStr(STRING),userCenterNid(STRING),baseSubject({}),accountStatus(STRING));RETURN {subjectInfo:{}}
-<VirtualTable-UserRoleAssignments "roleAssign">.select([["userId","eq",userIdStr],["status","eq","active"]],null,[0,200],["roleCode"]) -> _rolesResult
-_assignedCodes([STRING]) = []
-FOR (_i(INT) = 0; _i < _rolesResult.dataArray.length; _i++)
--_assignedCodes.push(_rolesResult.dataArray[_i].roleCode)
-_roleCodes([STRING]) = []
-IF _assignedCodes.length > 0
--<VirtualTable-UserRoles "roles">.select([["roleCode","in",_assignedCodes],["status","eq","active"]],null,[0,200],["roleCode"]) -> _activeRolesResult
--FOR (_k(INT) = 0; _k < _activeRolesResult.dataArray.length; _k++)
---_roleCodes.push(_activeRolesResult.dataArray[_k].roleCode)
-<VirtualTable-UserDepartments "userDepts">.select([["userId","eq",userIdStr],["status","eq","active"]],null,[0,50],null) -> _deptsResult
-_assignedDeptCodes([STRING]) = []
-FOR (_j(INT) = 0; _j < _deptsResult.dataArray.length; _j++)
--_assignedDeptCodes.push(_deptsResult.dataArray[_j].deptCode)
-// 同 roles：DisableDepartment 不动 UserDepartments，必须再用部门目录过滤掉 disabled 节点
-_activeDeptCatalog([STRING]) = []
-IF _assignedDeptCodes.length > 0
--<VirtualTable-Departments "depts">.select([["deptCode","in",_assignedDeptCodes],["status","eq","active"]],null,[0,50],["deptCode"]) -> _activeDeptsResult
--FOR (_m(INT) = 0; _m < _activeDeptsResult.dataArray.length; _m++)
---_activeDeptCatalog.push(_activeDeptsResult.dataArray[_m].deptCode)
-_deptCodes([STRING]) = []
-_primaryDeptCode(STRING) = ""
-FOR (_n(INT) = 0; _n < _deptsResult.dataArray.length; _n++)
--IF _activeDeptCatalog.indexOf(_deptsResult.dataArray[_n].deptCode) >= 0
---_deptCodes.push(_deptsResult.dataArray[_n].deptCode)
---IF _deptsResult.dataArray[_n].isPrimary
----_primaryDeptCode = _deptsResult.dataArray[_n].deptCode
-_acct(STRING) = (accountStatus != "" && accountStatus != NULL) ? accountStatus : "active"
-_subj({}) = {userId:userIdStr,userCenterNid:userCenterNid,userSpaceCode:"default",roles:_roleCodes,accountStatus:_acct,departmentIds:_deptCodes,primaryDepartmentId:_primaryDeptCode,mfaVerified:false,userType:"",securityLevel:"",tags:[]}
-RETURN {subjectInfo:_subj}

METHOD writeAudit(userId(STRING),accountInput(STRING),providerCode(STRING),eventType(STRING),result(STRING),failureReason(STRING));RETURN {success:BOOL}
-<VirtualTable-LoginAuditLogs "audit">.insert({userId:userId,accountInput:accountInput,providerCode:providerCode,eventType:eventType,result:result,failureReason:failureReason,ip:"",userAgent:""}) -> _insertResult
-RETURN {success:_insertResult.success}

// 凡是修改用户 roles / departmentIds / primaryDepartmentId 的操作完成后调用，
// 撤销当前用户中心下该用户全部 token，强制下次访问重新登录并拉新 subjectInfo。
// 静默失败（配置缺失等）只返回 success=false，调用方按需决定是否阻断业务。
METHOD revokeUserTokensInCurrentRealm(userIdStr(STRING));RETURN {success:BOOL,message:STRING,affectedCount:INT}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,affectedCount:0}
-<TokenIssuer "tokenIssuer">.revokeAllUserTokens(userIdStr, _ucResult.realmCode) -> _r
-RETURN {success:_r.success,message:"",affectedCount:_r.affectedCount}

// v0.2 后三个 load* METHOD 都走 resolveUserCenterNid（内部读 SYSENV.currentApp.nid）拿权威 nid，
// 不再校验入参 userCenterNid；env 与 userSpaceCode 入参保留：phase-1 校验 env 必须为 "preview"
// 且 userSpaceCode 必须为 "default"（缺省时按这两个默认值处理），便于未来扩展多空间不变 API 形状。
METHOD loadUserCenterInfo(env(STRING),userSpaceCode(STRING));RETURN {success:BOOL,message:STRING,userCenterNid:STRING,userSpaceCode:STRING,displayName:STRING,status:STRING,roleCatalogVersion:STRING,departmentTreeVersion:STRING,subjectSchemaVersion:STRING,updatedAt:INT}
-_envValue(STRING) = (env != "" && env != NULL) ? env : "preview"
-_space(STRING) = (userSpaceCode != "" && userSpaceCode != NULL) ? userSpaceCode : "default"
-IF _envValue != "preview"
--RETURN {success:false,message:("env '" + _envValue + "' not supported in phase-1; only 'preview'"),userCenterNid:"",userSpaceCode:_space,displayName:"",status:"",roleCatalogVersion:"",departmentTreeVersion:"",subjectSchemaVersion:"",updatedAt:0}
-IF _space != "default"
--RETURN {success:false,message:("userSpaceCode '" + _space + "' not supported in phase-1; only 'default'"),userCenterNid:"",userSpaceCode:_space,displayName:"",status:"",roleCatalogVersion:"",departmentTreeVersion:"",subjectSchemaVersion:"",updatedAt:0}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,userCenterNid:_ucResult.nid,userSpaceCode:_space,displayName:_ucResult.displayName,status:_ucResult.status,roleCatalogVersion:"",departmentTreeVersion:"",subjectSchemaVersion:"",updatedAt:_ucResult.updatedAt}
-RETURN {success:true,message:"",userCenterNid:_ucResult.nid,userSpaceCode:_space,displayName:_ucResult.displayName,status:_ucResult.status,roleCatalogVersion:"v1",departmentTreeVersion:"v1",subjectSchemaVersion:"v1",updatedAt:_ucResult.updatedAt}

METHOD loadRoleCatalog(env(STRING),userSpaceCode(STRING));RETURN {success:BOOL,message:STRING,roles:[{}]}
-_envValue(STRING) = (env != "" && env != NULL) ? env : "preview"
-_space(STRING) = (userSpaceCode != "" && userSpaceCode != NULL) ? userSpaceCode : "default"
-IF _envValue != "preview"
--RETURN {success:false,message:("env '" + _envValue + "' not supported in phase-1"),roles:[]}
-IF _space != "default"
--RETURN {success:false,message:("userSpaceCode '" + _space + "' not supported in phase-1"),roles:[]}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,roles:[]}
-<VirtualTable-UserRoles "roles">.select(null,[["sort","asc"]],[0,500],null) -> _listResult
-RETURN {success:true,message:"",roles:_listResult.dataArray}

METHOD loadDepartmentTree(env(STRING),userSpaceCode(STRING));RETURN {success:BOOL,message:STRING,departments:[{}]}
-_envValue(STRING) = (env != "" && env != NULL) ? env : "preview"
-_space(STRING) = (userSpaceCode != "" && userSpaceCode != NULL) ? userSpaceCode : "default"
-IF _envValue != "preview"
--RETURN {success:false,message:("env '" + _envValue + "' not supported in phase-1"),departments:[]}
-IF _space != "default"
--RETURN {success:false,message:("userSpaceCode '" + _space + "' not supported in phase-1"),departments:[]}
-resolveUserCenterNid() -> _ucResult
-IF !_ucResult.success
--RETURN {success:false,message:_ucResult.message,departments:[]}
-<VirtualTable-Departments "depts">.select(null,[["sort","asc"],["deptCode","asc"]],[0,500],null) -> _listResult
-RETURN {success:true,message:"",departments:_listResult.dataArray}

// 幂等 UserCenterConfig 设置项初始化 / 修复实现。v0.2 后 nid 不再由配置承载：
// - 0 行 → insert 新行（默认 displayName / status="active" 等设置项；nid 字段不再写，留空作为兼容字段）
// - 1+ 行 → 选 target 行 update 成 displayName / defaults / status="active"；多余行 disable
//   目标行优先级：第一个 status=active 行 > 第一行
// update 后立刻 re-select target 行做 post-verify：若 status 未持久化，返回 success=false。
METHOD ensureUserCenterConfig(displayName(STRING),defaultRoleCode(STRING),sessionDuration(INT),maxConcurrent(INT),permissionCenterBackendNid(INT),permissionCenterUrl(STRING));RETURN {success:BOOL,message:STRING,status:STRING,disabledRowCount:INT,rowsBefore:INT,targetRowId:INT,targetAction:STRING,postDisplayName:STRING,postStatus:STRING,postPermissionCenterBackendNid:INT,postPermissionCenterUrl:STRING}
-_session(INT) = sessionDuration > 0 ? sessionDuration : 7200
-_concurrent(INT) = maxConcurrent > 0 ? maxConcurrent : 5
-<VirtualTable-UserCenterConfig "config">.select(null,[["_id","asc"]],[0,100],null) -> _all
-_rowsBefore(INT) = _all.dataArray.length
-IF _rowsBefore == 0
--<VirtualTable-UserCenterConfig "config">.insert({env:"preview",displayName:displayName,defaultRoleCode:defaultRoleCode,registrationOpen:true,requireApproval:false,sessionDuration:_session,maxConcurrent:_concurrent,status:"active",permissionCenterBackendNid:permissionCenterBackendNid,permissionCenterUrl:permissionCenterUrl}) -> _insertResult
--IF !_insertResult.success
---RETURN {success:false,message:_insertResult.message,status:"",disabledRowCount:0,rowsBefore:0,targetRowId:0,targetAction:"insert_failed",postDisplayName:"",postStatus:"",postPermissionCenterBackendNid:0,postPermissionCenterUrl:""}
--RETURN {success:true,message:"inserted",status:"active",disabledRowCount:0,rowsBefore:0,targetRowId:(_insertResult.dataId ? _insertResult.dataId : 0),targetAction:"insert",postDisplayName:displayName,postStatus:"active",postPermissionCenterBackendNid:permissionCenterBackendNid,postPermissionCenterUrl:permissionCenterUrl}
-_targetIdx(INT) = -1
-_action(STRING) = ""
-FOR (_i(INT) = 0; _i < _all.dataArray.length; _i++)
--IF _targetIdx == -1 && _all.dataArray[_i].status == "active"
---_targetIdx = _i
---_action = "update_active_row"
-IF _targetIdx == -1
--_targetIdx = 0
--_action = "update_first_row"
-_curRow({}) = _all.dataArray[_targetIdx]
-_targetId(INT) = _curRow._id ? _curRow._id : 0
-_finalDisplayName(STRING) = (displayName != "" && displayName != NULL) ? displayName : (_curRow.displayName ? _curRow.displayName : "")
-_finalDefaultRole(STRING) = (defaultRoleCode != "" && defaultRoleCode != NULL) ? defaultRoleCode : (_curRow.defaultRoleCode ? _curRow.defaultRoleCode : "")
-_finalPcNid(INT) = permissionCenterBackendNid > 0 ? permissionCenterBackendNid : (_curRow.permissionCenterBackendNid ? _curRow.permissionCenterBackendNid : 0)
-_finalPcUrl(STRING) = (permissionCenterUrl != "" && permissionCenterUrl != NULL) ? permissionCenterUrl : (_curRow.permissionCenterUrl ? _curRow.permissionCenterUrl : "")
-<VirtualTable-UserCenterConfig "config">.update([["_id","eq",_targetId]],[["env","set","preview"],["displayName","set",_finalDisplayName],["defaultRoleCode","set",_finalDefaultRole],["sessionDuration","set",_session],["maxConcurrent","set",_concurrent],["status","set","active"],["permissionCenterBackendNid","set",_finalPcNid],["permissionCenterUrl","set",_finalPcUrl]],1) -> _updateResult
-IF !_updateResult.success
--RETURN {success:false,message:_updateResult.message,status:"",disabledRowCount:0,rowsBefore:_rowsBefore,targetRowId:_targetId,targetAction:(_action + "_failed"),postDisplayName:"",postStatus:"",postPermissionCenterBackendNid:0,postPermissionCenterUrl:""}
-IF _updateResult.affect == 0
--RETURN {success:false,message:("update matched 0 rows; targetRowId=" + _targetId + " did not exist in DB"),status:"",disabledRowCount:0,rowsBefore:_rowsBefore,targetRowId:_targetId,targetAction:(_action + "_zero_affect"),postDisplayName:"",postStatus:"",postPermissionCenterBackendNid:0,postPermissionCenterUrl:""}
-<VirtualTable-UserCenterConfig "config">.select([["_id","eq",_targetId]],null,[0,1],null) -> _verify
-IF _verify.dataArray.length == 0
--RETURN {success:false,message:"post-update verify failed: target row vanished",status:"",disabledRowCount:0,rowsBefore:_rowsBefore,targetRowId:_targetId,targetAction:(_action + "_vanished"),postDisplayName:"",postStatus:"",postPermissionCenterBackendNid:0,postPermissionCenterUrl:""}
-_postRow({}) = _verify.dataArray[0]
-_postDisplayName(STRING) = _postRow.displayName ? _postRow.displayName : ""
-_postStatus(STRING) = _postRow.status ? _postRow.status : ""
-_postPcNid(INT) = _postRow.permissionCenterBackendNid ? _postRow.permissionCenterBackendNid : 0
-_postPcUrl(STRING) = _postRow.permissionCenterUrl ? _postRow.permissionCenterUrl : ""
-IF _postStatus != "active"
--RETURN {success:false,message:("post-update verify failed: status is '" + _postStatus + "' but expected 'active'"),status:_postStatus,disabledRowCount:0,rowsBefore:_rowsBefore,targetRowId:_targetId,targetAction:(_action + "_not_persisted"),postDisplayName:_postDisplayName,postStatus:_postStatus,postPermissionCenterBackendNid:_postPcNid,postPermissionCenterUrl:_postPcUrl}
-_disabledCount(INT) = 0
-IF _all.dataArray.length > 1
--FOR (_k(INT) = 0; _k < _all.dataArray.length; _k++)
---IF _k != _targetIdx
----<VirtualTable-UserCenterConfig "config">.update([["_id","eq",_all.dataArray[_k]._id]],[["status","set","disabled"]],1) -> _disResult
----IF _disResult.success
-----_disabledCount = _disabledCount + 1
-_msg(STRING) = _disabledCount > 0 ? ("updated; disabled " + _disabledCount + " stale row(s)") : "updated"
-RETURN {success:true,message:_msg,status:"active",disabledRowCount:_disabledCount,rowsBefore:_rowsBefore,targetRowId:_targetId,targetAction:_action,postDisplayName:_postDisplayName,postStatus:_postStatus,postPermissionCenterBackendNid:_postPcNid,postPermissionCenterUrl:_postPcUrl}

// 联调用：尝试创建角色，已存在则跳过。
METHOD tryCreateRole(roleCode(STRING),name(STRING),description(STRING));RETURN {created:BOOL,existed:BOOL}
-<VirtualTable-UserRoles "roles">.count([["roleCode","eq",roleCode]]) -> _ex
-IF _ex.count > 0
--RETURN {created:false,existed:true}
-<VirtualTable-UserRoles "roles">.insert({roleCode:roleCode,name:name,description:description,status:"active",sort:0}) -> _ins
-RETURN {created:_ins.success,existed:false}

// 联调用：批量预置默认角色（admin / teacher / viewer），已存在的不重复创建。
METHOD seedDefaultRoles();RETURN {success:BOOL,message:STRING,created:[STRING],existing:[STRING]}
-_created([STRING]) = []
-_existing([STRING]) = []
-tryCreateRole("admin", "管理员", "系统管理员（默认初始化）") -> _r1
-IF _r1.created
--_created.push("admin")
-IF _r1.existed
--_existing.push("admin")
-tryCreateRole("teacher", "教师", "教师角色（默认初始化）") -> _r2
-IF _r2.created
--_created.push("teacher")
-IF _r2.existed
--_existing.push("teacher")
-tryCreateRole("viewer", "只读访客", "只读权限（默认初始化）") -> _r3
-IF _r3.created
--_created.push("viewer")
-IF _r3.existed
--_existing.push("viewer")
-RETURN {success:true,message:"",created:_created,existing:_existing}

# Backend Pipeline Funcs

</ServiceDomain-UserCenterService>
