import { EntityManager, getEntityManager, Query } from "relaen"; import { NfGroup } from "./entity/nfgroup"; import { NfNode } from "./entity/nfnode"; import { NfUser } from "./entity/nfuser"; import { NFEngine } from "./engine"; import { NFError } from "./error"; import { NFTaskUtil } from "./taskutil"; import { INFNextParam } from "./types"; import { NFUserManager } from "./usermanager"; /** * 任务服务:认领、指派、查询与完成。 * * `taskId` 为任务实例 id(`NfNode.nodeId`)。 */ export class NFTaskService{ /** * 直接指派办理人(独占,并收窄候选人列表)。 * * @param taskId - 任务实例 id * @param userId - 用户 id * @returns 更新后的任务节点 * @throws `1200` 任务不存在 * @throws `1201` 任务已完成 * @throws `1204` 用户不存在 */ async setAssignee(taskId:number|string,userId:number|string){ const node = await this.getActiveTask(taskId); const uid = await this.ensureUserId(userId); node.assignee = uid; node.candidateUsers = NFTaskUtil.formatIdList([uid]); await node.save(); return node; } /** * 设置候选人(清除办理人,回到待认领)。 * * @param taskId - 任务实例 id * @param userIds - 用户 id 数组 * @returns 更新后的任务节点 * @throws `1200` 任务不存在 * @throws `1201` 任务已完成 * @throws `1204` 用户不存在 */ async setCandidates(taskId:number|string,userIds:(number|string)[]){ const node = await this.getActiveTask(taskId); const ids = await this.normalizeUserIds(userIds); node.assignee = null; node.candidateUsers = NFTaskUtil.formatIdList(ids); node.candidateGroups = null; await node.save(); return node; } /** * 设置候选组(展开组成员写入候选人,清除办理人)。 * * @param taskId - 任务实例 id * @param groupIds - 组 id 数组;空数组则清空候选人 * @returns 更新后的任务节点 * @throws `1200` 任务不存在 * @throws `1201` 任务已完成 */ async setCandidateGroups(taskId:number|string,groupIds:(number|string)[]){ const node = await this.getActiveTask(taskId); if(!groupIds || groupIds.length === 0){ node.assignee = null; node.candidateUsers = ',,'; node.candidateGroups = null; await node.save(); return node; } const ids = await this.normalizeGroupIds(groupIds); const userIds = ids.length > 0 ? await NFUserManager.getUserIdsByGroupIds(ids) : []; node.assignee = null; node.candidateGroups = NFTaskUtil.formatIdList(ids); node.candidateUsers = NFTaskUtil.formatIdList(userIds); await node.save(); return node; } /** * 获取当前办理人。 * * @param taskId - 任务实例 id * @returns 用户实体;未指派时为 `null` * @throws `1200` 任务不存在 */ async getAssignee(taskId:number|string):Promise{ const node = await this.getTask(taskId); if(node.assignee == null){ return null; } return await NfUser.find(node.assignee); } /** * 获取候选人用户列表。 * * @param taskId - 任务实例 id * @returns 用户实体数组 * @throws `1200` 任务不存在 */ async getCandidates(taskId:number|string):Promise{ const node = await this.getTask(taskId); const ids = NFTaskUtil.parseIdList(node.candidateUsers); if(ids.length === 0){ return []; } const em:EntityManager = await getEntityManager(); const query:Query = em.createQuery(NfUser.name); const rows = await query.select('*') .where({userId:{rel:'in',value:ids}}) .getResultList(); await em.close(); return rows || []; } /** * 获取候选组列表。 * * @param taskId - 任务实例 id * @returns 组实体数组 * @throws `1200` 任务不存在 */ async getCandidateGroups(taskId:number|string):Promise{ const node = await this.getTask(taskId); if(!node.candidateGroups){ return []; } const ids = NFTaskUtil.parseIdList(node.candidateGroups); if(ids.length === 0){ return []; } const em:EntityManager = await getEntityManager(); const query:Query = em.createQuery(NfGroup.name); const rows = await query.select('*') .where({groupId:{rel:'in',value:ids}}) .getResultList(); await em.close(); return rows || []; } /** * 认领任务(候选人 / 候选组任务)。已由本人认领时幂等返回。 * * @param taskId - 任务实例 id * @param userId - 认领人用户 id * @returns 更新后的任务节点 * @throws `1202` 已被他人认领 * @throws `1203` 不在候选人 / 候选组中 * @throws `1204` 用户不存在 */ async claim(taskId:number|string,userId:number|string){ const node = await this.getActiveTask(taskId); const uid = await this.ensureUserId(userId); if(node.assignee != null){ if(Number(node.assignee) === uid){ return node; // 幂等 } throw new NFError("1202"); } const groups = await NFUserManager.getGroupIdsByUserId(uid); if(!NFTaskUtil.isCandidate(node,uid,groups)){ throw new NFError("1203"); } node.assignee = uid; // 认领后独占:收窄待办可见范围 node.candidateUsers = NFTaskUtil.formatIdList([uid]); await node.save(); return node; } /** * 取消认领,任务回到候选人池。 * * 使用当前 `candidateUsers`;若认领时已收窄,需先 {@link setCandidates} 恢复。 * * @param taskId - 任务实例 id * @returns 更新后的任务节点 * @throws `1200` 任务不存在 * @throws `1201` 任务已完成 */ async unclaim(taskId:number|string){ const node = await this.getActiveTask(taskId); node.assignee = null; await node.save(); return node; } /** * 完成任务并推进流程。未认领时由办理人自动认领。 * * @param taskId - 任务实例 id * @param cfg - 办理参数(`userId` / `agree` / `reason` / `variables`) * @returns 是否完成 * @throws `1200` 任务或流程不存在 * @throws `1203` 未认领且不是候选人 * @throws `1205` 不是当前办理人 */ async finish(taskId:number|string,cfg?:INFNextParam){ const node = await this.getActiveTask(taskId); const processId = node.nfProcess?.processId; if(!processId){ throw new NFError("1200"); } const userId = cfg?.userId != null ? Number(cfg.userId) : node.assignee; // 未认领时:办理人必须是候选人,并自动认领 if(node.assignee == null){ const groups = userId != null ? await NFUserManager.getGroupIdsByUserId(userId) : []; if(userId == null || !NFTaskUtil.isCandidate(node,userId,groups)){ throw new NFError("1203"); } node.assignee = userId; node.candidateUsers = NFTaskUtil.formatIdList([userId]); await node.save(); }else if(userId != null && Number(node.assignee) !== Number(userId)){ throw new NFError("1205"); } const proc = await NFEngine.getInstance(processId); if(!proc){ throw new NFError("1200"); } await proc.next({ ...cfg, id: node.defId, userId: userId != null ? Number(userId) : node.assignee }); return true; } /** * 获取任务(不限是否完成)。 * * @param taskId - 任务实例 id * @returns 任务节点实体 * @throws `1200` 任务不存在或 id 非法 */ async getTask(taskId:number|string):Promise{ const id = Number(taskId); if(isNaN(id)){ throw new NFError("1200"); } const em:EntityManager = await getEntityManager(); const query:Query = em.createQuery(NfNode.name); const node = await query.select(['*','nfProcess.*']) .where({nodeId:id}) .getResult(); await em.close(); if(!node){ throw new NFError("1200"); } return node; } /** * 获取未完成任务。 * * @param taskId - 任务实例 id * @returns 活动任务节点 * @throws `1200` 任务不存在 * @throws `1201` 任务已完成 */ private async getActiveTask(taskId:number|string):Promise{ const node = await this.getTask(taskId); if(node.endTime){ throw new NFError("1201"); } return node; } /** * 校验用户存在并转为数字 id。 * * @param userId - 用户 id * @returns 数字用户 id * @throws `1204` 用户不存在或 id 非法 */ private async ensureUserId(userId:number|string):Promise{ const uid = Number(userId); if(isNaN(uid)){ throw new NFError("1204"); } const user = await NfUser.find(uid); if(!user){ throw new NFError("1204"); } return uid; } /** * 将用户 id 列表规范化为已存在的数字 id。 * * @param userIds - 用户 id 数组 * @returns 数字用户 id 数组 * @throws `1204` 任一用户不存在 */ private async normalizeUserIds(userIds:(number|string)[]):Promise{ const ids:number[] = []; for(let id of userIds || []){ ids.push(await this.ensureUserId(id)); } return ids; } /** * 将组 id 列表规范化为已存在的数字 id(非法或不存在的项跳过)。 * * @param groupIds - 组 id 数组 * @returns 数字组 id 数组 */ private async normalizeGroupIds(groupIds:(number|string)[]):Promise{ const ids:number[] = []; for(let g of groupIds || []){ const gid = Number(g); if(isNaN(gid)){ continue; } const group = await NfGroup.find(gid); if(group){ ids.push(gid); } } return ids; } }