cangku/backend/src/api/desktopExecutionTasks.js

263 lines
9.0 KiB
JavaScript

'use strict';
const express = require('express');
const authMiddleware = require('../middleware/auth');
const { listOnlineDesktopUsers } = require('../services/desktopPresenceService');
const { assertDesktopSession } = require('../services/desktopSessionService');
const { listSelectionFissionUserShops } = require('../services/planningSelectionFissionService');
const {
DesktopExecutionTaskError,
cancelDesktopExecutionTask,
claimDesktopExecutionTask,
completeDesktopExecutionTask,
createDesktopExecutionTask,
failDesktopExecutionTask,
getDesktopExecutionSummary,
listDesktopExecutionTasks,
retryDesktopExecutionTask,
updateDesktopExecutionProgress,
} = require('../services/desktopExecutionTaskService');
const router = express.Router();
router.use(authMiddleware);
function actorFromRequest(req) {
return {
userId: req.user?.sub ?? req.user?.id,
username: req.user?.username || '',
name: req.user?.name || req.user?.username || '',
role: req.user?.role || 'user',
};
}
function assertControlCenterRequest(req) {
if (req.user?.tokenType === 'desktop-session') {
throw new DesktopExecutionTaskError('该接口仅供总控页面使用', 403, 'DESKTOP_TASK_CONTROL_CENTER_REQUIRED');
}
if (req.user?.role !== 'admin') {
throw new DesktopExecutionTaskError('只有管理员可以管理桌面执行任务', 403, 'DESKTOP_TASK_ADMIN_REQUIRED');
}
}
async function validatePresaleAssignment(input = {}) {
if (String(input.taskType || input.type || '').trim().toLowerCase() !== 'presale') return input;
const assignedUserId = Number(input.assignedUserId || input.userId || 0);
const presence = await listOnlineDesktopUsers();
const operator = (presence.users || []).find((item) => (
Number(item.userId) === assignedUserId && item.isOnline
));
if (!operator) {
throw new DesktopExecutionTaskError(
'所选运营的桌面客户端当前不在线',
409,
'DESKTOP_TASK_OPERATOR_OFFLINE'
);
}
const operatorName = operator.name || operator.username;
const shopData = await listSelectionFissionUserShops(operatorName, { includeInactive: false });
const requestedShopIds = [
input.targetShopId,
...(Array.isArray(input.input?.targetShopIds) ? input.input.targetShopIds : []),
].map((value) => String(value || '').trim()).filter(Boolean);
const uniqueShopIds = [...new Set(requestedShopIds)];
if (!uniqueShopIds.length) {
throw new DesktopExecutionTaskError(
'请至少选择一个登录状态正常的目标店铺',
400,
'DESKTOP_TASK_SHOP_REQUIRED'
);
}
const selectableById = new Map(
(shopData.rows || []).filter((shop) => shop.selectable).map((shop) => [String(shop.shopId), shop])
);
const selectedShops = uniqueShopIds.map((shopId) => selectableById.get(shopId)).filter(Boolean);
if (selectedShops.length !== uniqueShopIds.length) {
throw new DesktopExecutionTaskError(
'目标店铺已离线或登录状态失效,请刷新后重新选择',
409,
'DESKTOP_TASK_SHOP_UNAVAILABLE'
);
}
return {
...input,
assignedUserId,
assignedUserName: operator.name || operator.username || '',
assignedUsername: operator.username || '',
targetShopId: selectedShops.length === 1 ? selectedShops[0].shopId : '',
targetShopName: selectedShops.length === 1 ? selectedShops[0].shopName : '',
input: {
...(input.input || {}),
targetShopIds: selectedShops.map((shop) => shop.shopId),
targetShopNames: selectedShops.map((shop) => shop.shopName),
},
};
}
async function assertDesktopRequest(req) {
if (req.user?.tokenType !== 'desktop-session') {
throw new DesktopExecutionTaskError('该接口仅供桌面客户端使用', 403, 'DESKTOP_TASK_CLIENT_REQUIRED');
}
const clientId = req.body?.clientId || req.query?.clientId || req.user?.clientId;
await assertDesktopSession(req.user, clientId);
return clientId;
}
function sendError(res, error, fallback) {
const status = error instanceof DesktopExecutionTaskError || Number(error.status)
? Number(error.status || 500)
: 500;
if (status >= 500) console.error(`[DesktopExecutionTask] ${fallback}:`, error);
return res.status(status).json({
success: false,
code: error.code || 'DESKTOP_TASK_SERVER_ERROR',
error: error.message || fallback,
});
}
router.get('/tasks', async (req, res) => {
try {
assertControlCenterRequest(req);
const data = await listDesktopExecutionTasks(req.query || {}, actorFromRequest(req));
res.setHeader('Cache-Control', 'no-store');
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '读取桌面任务失败');
}
});
router.get('/summary', async (req, res) => {
try {
assertControlCenterRequest(req);
const data = await getDesktopExecutionSummary(req.query || {}, actorFromRequest(req));
res.setHeader('Cache-Control', 'no-store');
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '读取桌面任务概览失败');
}
});
router.get('/operators', async (req, res) => {
try {
assertControlCenterRequest(req);
const presence = await listOnlineDesktopUsers();
const users = (presence.users || []).map((user) => ({
userId: Number(user.userId || 0),
username: user.username || '',
name: user.name || user.username || '',
departmentName: user.departmentName || '',
isOnline: Boolean(user.isOnline),
onlineDeviceCount: Number(user.activeDeviceCount || 0),
lastSeenAt: user.lastSeenAt || null,
}));
return res.json({ success: true, data: { users, offlineAfterSeconds: presence.offlineAfterSeconds } });
} catch (error) {
return sendError(res, error, '读取桌面运营列表失败');
}
});
router.get('/operators/:userId/shops', async (req, res) => {
try {
assertControlCenterRequest(req);
const presence = await listOnlineDesktopUsers();
const user = (presence.users || []).find((item) => Number(item.userId) === Number(req.params.userId));
if (!user) {
throw new DesktopExecutionTaskError('未找到该桌面运营记录', 404, 'DESKTOP_TASK_OPERATOR_NOT_FOUND');
}
const userName = user.name || user.username;
const data = await listSelectionFissionUserShops(userName, { includeInactive: false });
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '读取运营店铺状态失败');
}
});
router.post('/tasks', async (req, res) => {
try {
assertControlCenterRequest(req);
const input = await validatePresaleAssignment(req.body || {});
const data = await createDesktopExecutionTask(input, actorFromRequest(req));
return res.status(201).json({ success: true, data });
} catch (error) {
return sendError(res, error, '下发桌面任务失败');
}
});
router.post('/tasks/claim', async (req, res) => {
try {
const clientId = await assertDesktopRequest(req);
const data = await claimDesktopExecutionTask(
{ ...(req.body || {}), clientId },
actorFromRequest(req)
);
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '领取桌面任务失败');
}
});
router.post('/tasks/:taskId/progress', async (req, res) => {
try {
const clientId = await assertDesktopRequest(req);
const data = await updateDesktopExecutionProgress(
req.params.taskId,
{ ...(req.body || {}), clientId },
actorFromRequest(req)
);
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '回传桌面任务进度失败');
}
});
router.post('/tasks/:taskId/complete', async (req, res) => {
try {
const clientId = await assertDesktopRequest(req);
const data = await completeDesktopExecutionTask(
req.params.taskId,
{ ...(req.body || {}), clientId },
actorFromRequest(req)
);
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '完成桌面任务失败');
}
});
router.post('/tasks/:taskId/fail', async (req, res) => {
try {
const clientId = await assertDesktopRequest(req);
const data = await failDesktopExecutionTask(
req.params.taskId,
{ ...(req.body || {}), clientId },
actorFromRequest(req)
);
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '回传桌面任务失败结果失败');
}
});
router.post('/tasks/:taskId/cancel', async (req, res) => {
try {
assertControlCenterRequest(req);
const data = await cancelDesktopExecutionTask(req.params.taskId, actorFromRequest(req));
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '取消桌面任务失败');
}
});
router.post('/tasks/:taskId/retry', async (req, res) => {
try {
assertControlCenterRequest(req);
const data = await retryDesktopExecutionTask(req.params.taskId, actorFromRequest(req));
return res.json({ success: true, data });
} catch (error) {
return sendError(res, error, '重试桌面任务失败');
}
});
module.exports = router;