- 数据库:新增 user_groups 多对多关联表,启动时自动迁移旧 group_id 数据 - 后端 users:所有查询聚合多工作组信息,增删改接口支持 group_ids 数组;新增 POST /users/batch 批量添加接口(事务+逐行错误反馈) - 后端 export/hours/activities:全面兼容多工作组关联查询 - 前端 Users:新增/编辑改为工作组多选复选框;新增批量添加模态框(粘贴解析+实时校验表+提交汇总) - 前端列表/详情:工作组以多标签形式展示,搜索支持工作组名称
This commit is contained in:
@@ -105,6 +105,26 @@ export async function initDb() {
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
try { d.execCompat(schema); d.save(); } catch (e) { console.error('Schema init error', e); }
|
||||
|
||||
try {
|
||||
const ugExists = d.prepareCompat(`
|
||||
SELECT name FROM sqlite_master WHERE type='table' AND name='user_groups'
|
||||
`).get();
|
||||
if (ugExists) {
|
||||
const legacyRows = d.prepareCompat(`
|
||||
SELECT COUNT(*) AS c FROM users WHERE group_id IS NOT NULL
|
||||
AND id NOT IN (SELECT DISTINCT user_id FROM user_groups)
|
||||
`).get();
|
||||
if (legacyRows && legacyRows.c > 0) {
|
||||
d.prepareCompat(`
|
||||
INSERT OR IGNORE INTO user_groups (user_id, group_id)
|
||||
SELECT id, group_id FROM users WHERE group_id IS NOT NULL
|
||||
`).run();
|
||||
d.save();
|
||||
console.log(`[DB] Migrated ${legacyRows.c} users from legacy group_id to user_groups`);
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error('User_groups migration error', e); }
|
||||
|
||||
const adminCount = d.prepareCompat('SELECT COUNT(*) as c FROM admins').get();
|
||||
if (!adminCount || adminCount.c === 0) {
|
||||
const hash = bcrypt.hashSync('admin123', 10);
|
||||
|
||||
@@ -13,6 +13,14 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
UNIQUE(class, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
user_id INTEGER NOT NULL,
|
||||
group_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, group_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES work_groups(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
|
||||
@@ -103,17 +103,27 @@ router.get('/:id/participants', (req: Request, res: Response) => {
|
||||
let users: any[];
|
||||
if (groupIds.length === 0) {
|
||||
users = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name
|
||||
FROM users u LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
(SELECT GROUP_CONCAT(g.name)
|
||||
FROM user_groups ug2
|
||||
LEFT JOIN work_groups g ON ug2.group_id = g.id
|
||||
WHERE ug2.user_id = u.id) AS group_name
|
||||
FROM users u
|
||||
GROUP BY u.id
|
||||
ORDER BY u.class, u.name
|
||||
`).all();
|
||||
} else {
|
||||
const ph = groupIds.map(() => '?').join(',');
|
||||
users = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
(SELECT GROUP_CONCAT(g.name)
|
||||
FROM user_groups ug2
|
||||
LEFT JOIN work_groups g ON ug2.group_id = g.id
|
||||
WHERE ug2.user_id = u.id) AS group_name
|
||||
FROM users u
|
||||
LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
WHERE u.group_id IN (${ph}) OR u.group_id IS NULL
|
||||
WHERE u.id IN (SELECT DISTINCT user_id FROM user_groups WHERE group_id IN (${ph}))
|
||||
OR u.id NOT IN (SELECT DISTINCT user_id FROM user_groups)
|
||||
GROUP BY u.id
|
||||
ORDER BY u.class, u.name
|
||||
`).all(...groupIds);
|
||||
}
|
||||
|
||||
+59
-16
@@ -4,17 +4,34 @@ import { authMiddleware, AuthRequest } from '../middleware/auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
function withGroupNames(rows: any[]): any[] {
|
||||
return rows.map((r) => {
|
||||
const names: string[] = [];
|
||||
if (r._group_names_csv) {
|
||||
for (const s of String(r._group_names_csv).split(',')) {
|
||||
if (s && s.trim()) names.push(s.trim());
|
||||
}
|
||||
}
|
||||
return { ...r, group_name: names.join('、') || null };
|
||||
});
|
||||
}
|
||||
|
||||
router.post('/', authMiddleware, (req: AuthRequest, res) => {
|
||||
const { include_users, include_activities, include_hours, include_user_totals, include_user_activities, include_activity_matrix } = req.body || {};
|
||||
const db = getDb();
|
||||
const result: any = {};
|
||||
|
||||
if (include_users) {
|
||||
result.users = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name
|
||||
FROM users u LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
const rows = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
GROUP_CONCAT(g.name) AS _group_names_csv
|
||||
FROM users u
|
||||
LEFT JOIN user_groups ug ON u.id = ug.user_id
|
||||
LEFT JOIN work_groups g ON ug.group_id = g.id
|
||||
GROUP BY u.id
|
||||
ORDER BY u.class, u.name
|
||||
`).all();
|
||||
result.users = withGroupNames(rows).map(({ _group_names_csv: _, ...rest }) => rest);
|
||||
}
|
||||
if (include_activities) {
|
||||
result.activities = db.prepareCompat(`
|
||||
@@ -27,35 +44,49 @@ router.post('/', authMiddleware, (req: AuthRequest, res) => {
|
||||
`).all();
|
||||
}
|
||||
if (include_hours) {
|
||||
result.hours = db.prepareCompat(`
|
||||
const rows = db.prepareCompat(`
|
||||
SELECT h.hours, h.remark, h.created_at,
|
||||
a.title AS activity_title, a.start_time, a.end_time,
|
||||
u.class, u.name, u.volunteer_no, g.name AS group_name
|
||||
u.class, u.name, u.volunteer_no,
|
||||
(SELECT GROUP_CONCAT(g2.name)
|
||||
FROM user_groups ug2
|
||||
LEFT JOIN work_groups g2 ON ug2.group_id = g2.id
|
||||
WHERE ug2.user_id = u.id) AS _group_names_csv
|
||||
FROM hours h
|
||||
JOIN activities a ON h.activity_id = a.id
|
||||
JOIN users u ON h.user_id = u.id
|
||||
LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
ORDER BY a.start_time DESC, u.class, u.name
|
||||
`).all();
|
||||
result.hours = withGroupNames(rows).map(({ _group_names_csv: _, ...rest }) => rest);
|
||||
}
|
||||
if (include_user_totals) {
|
||||
result.user_totals = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name,
|
||||
const rows = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
(SELECT GROUP_CONCAT(g2.name)
|
||||
FROM user_groups ug2
|
||||
LEFT JOIN work_groups g2 ON ug2.group_id = g2.id
|
||||
WHERE ug2.user_id = u.id) AS _group_names_csv,
|
||||
COALESCE(SUM(h.hours), 0) AS total_hours,
|
||||
COUNT(DISTINCT h.activity_id) AS activity_count
|
||||
FROM users u
|
||||
LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
LEFT JOIN hours h ON h.user_id = u.id
|
||||
GROUP BY u.id
|
||||
ORDER BY total_hours DESC, u.class, u.name
|
||||
`).all();
|
||||
result.user_totals = withGroupNames(rows).map(({ _group_names_csv: _, ...rest }) => rest);
|
||||
}
|
||||
if (include_user_activities) {
|
||||
const users = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name
|
||||
FROM users u LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
const userRows = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
(SELECT GROUP_CONCAT(g2.name)
|
||||
FROM user_groups ug2
|
||||
LEFT JOIN work_groups g2 ON ug2.group_id = g2.id
|
||||
WHERE ug2.user_id = u.id) AS _group_names_csv
|
||||
FROM users u
|
||||
GROUP BY u.id
|
||||
ORDER BY u.class, u.name
|
||||
`).all() as any[];
|
||||
const users = withGroupNames(userRows).map(({ _group_names_csv: _, ...rest }) => rest);
|
||||
result.user_activities = users.map((u) => {
|
||||
const acts = db.prepareCompat(`
|
||||
SELECT a.title, a.start_time, a.end_time, h.hours, h.remark
|
||||
@@ -69,11 +100,17 @@ router.post('/', authMiddleware, (req: AuthRequest, res) => {
|
||||
const activities = db.prepareCompat(`
|
||||
SELECT id, title, start_time FROM activities ORDER BY start_time DESC, id DESC
|
||||
`).all() as any[];
|
||||
const users = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name
|
||||
FROM users u LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
const userRows = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
(SELECT GROUP_CONCAT(g2.name)
|
||||
FROM user_groups ug2
|
||||
LEFT JOIN work_groups g2 ON ug2.group_id = g2.id
|
||||
WHERE ug2.user_id = u.id) AS _group_names_csv
|
||||
FROM users u
|
||||
GROUP BY u.id
|
||||
ORDER BY u.class, u.name
|
||||
`).all() as any[];
|
||||
const users = withGroupNames(userRows);
|
||||
const hoursRows = db.prepareCompat(`
|
||||
SELECT user_id, activity_id, hours FROM hours
|
||||
`).all() as any[];
|
||||
@@ -85,7 +122,13 @@ router.post('/', authMiddleware, (req: AuthRequest, res) => {
|
||||
result.activity_matrix = {
|
||||
activities,
|
||||
users: users.map((u) => {
|
||||
const row: Record<string, any> = { id: u.id, class: u.class, name: u.name, volunteer_no: u.volunteer_no, group_name: u.group_name };
|
||||
const row: Record<string, any> = {
|
||||
id: u.id,
|
||||
class: u.class,
|
||||
name: u.name,
|
||||
volunteer_no: u.volunteer_no,
|
||||
group_name: u.group_name,
|
||||
};
|
||||
for (const a of activities) {
|
||||
row[String(a.id)] = Number((matrix[`${u.id}|${a.id}`] || 0).toFixed(2));
|
||||
}
|
||||
|
||||
@@ -44,10 +44,12 @@ router.get('/activity/:id', (req: Request, res: Response) => {
|
||||
const rows = db.prepareCompat(`
|
||||
SELECT h.id, h.hours, h.remark, h.created_at,
|
||||
u.id AS user_id, u.class, u.name, u.volunteer_no,
|
||||
g.name AS group_name
|
||||
(SELECT GROUP_CONCAT(g.name)
|
||||
FROM user_groups ug
|
||||
LEFT JOIN work_groups g ON ug.group_id = g.id
|
||||
WHERE ug.user_id = u.id) AS group_name
|
||||
FROM hours h
|
||||
JOIN users u ON h.user_id = u.id
|
||||
LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
WHERE h.activity_id = ?
|
||||
ORDER BY u.class, u.name
|
||||
`).all(id);
|
||||
|
||||
+151
-18
@@ -4,44 +4,77 @@ import { authMiddleware, AuthRequest } from '../middleware/auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
function parseCsvIds(s: any): number[] {
|
||||
if (!s) return [];
|
||||
return String(s).split(',').map(x => Number(x.trim())).filter(n => !isNaN(n) && n > 0);
|
||||
}
|
||||
|
||||
function parseCsvStrings(s: any): string[] {
|
||||
if (!s) return [];
|
||||
return String(s).split(',').filter(x => x && x.trim().length > 0);
|
||||
}
|
||||
|
||||
function enrichUser(u: any) {
|
||||
if (!u) return u;
|
||||
return {
|
||||
...u,
|
||||
group_ids: parseCsvIds(u.group_ids_csv),
|
||||
group_names: parseCsvStrings(u.group_names_csv),
|
||||
group_name: parseCsvStrings(u.group_names_csv).join('、') || null,
|
||||
group_id: parseCsvIds(u.group_ids_csv)[0] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
router.get('/', authMiddleware, (req: AuthRequest, res: Response) => {
|
||||
const db = getDb();
|
||||
const list = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, u.group_id,
|
||||
g.name AS group_name,
|
||||
const rows = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
GROUP_CONCAT(g.id) AS group_ids_csv,
|
||||
GROUP_CONCAT(g.name) AS group_names_csv,
|
||||
COALESCE((SELECT SUM(h.hours) FROM hours h WHERE h.user_id = u.id), 0) AS total_hours
|
||||
FROM users u
|
||||
LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
LEFT JOIN user_groups ug ON u.id = ug.user_id
|
||||
LEFT JOIN work_groups g ON ug.group_id = g.id
|
||||
GROUP BY u.id
|
||||
ORDER BY u.class ASC, u.name ASC
|
||||
`).all();
|
||||
res.json(list);
|
||||
res.json(rows.map(enrichUser));
|
||||
});
|
||||
|
||||
router.get('/search', (req: Request, res: Response) => {
|
||||
const name = String(req.query.name || '').trim();
|
||||
if (!name) return res.json([]);
|
||||
const db = getDb();
|
||||
const list = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name
|
||||
const rows = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
GROUP_CONCAT(g.id) AS group_ids_csv,
|
||||
GROUP_CONCAT(g.name) AS group_names_csv
|
||||
FROM users u
|
||||
LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
LEFT JOIN user_groups ug ON u.id = ug.user_id
|
||||
LEFT JOIN work_groups g ON ug.group_id = g.id
|
||||
WHERE u.name LIKE ?
|
||||
GROUP BY u.id
|
||||
ORDER BY u.class ASC
|
||||
`).all(`%${name}%`);
|
||||
res.json(list);
|
||||
res.json(rows.map(enrichUser));
|
||||
});
|
||||
|
||||
router.get('/:id', (req: Request, res: Response) => {
|
||||
const id = Number(req.params.id);
|
||||
const db = getDb();
|
||||
const user = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no, u.group_id, g.name AS group_name,
|
||||
const raw = db.prepareCompat(`
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
GROUP_CONCAT(g.id) AS group_ids_csv,
|
||||
GROUP_CONCAT(g.name) AS group_names_csv,
|
||||
COALESCE((SELECT SUM(h.hours) FROM hours h WHERE h.user_id = u.id), 0) AS total_hours
|
||||
FROM users u
|
||||
LEFT JOIN work_groups g ON u.group_id = g.id
|
||||
LEFT JOIN user_groups ug ON u.id = ug.user_id
|
||||
LEFT JOIN work_groups g ON ug.group_id = g.id
|
||||
WHERE u.id = ?
|
||||
GROUP BY u.id
|
||||
`).get(id) as any;
|
||||
if (!user) return res.status(404).json({ error: '部员不存在' });
|
||||
if (!raw) return res.status(404).json({ error: '部员不存在' });
|
||||
const user = enrichUser(raw);
|
||||
const activities = db.prepareCompat(`
|
||||
SELECT a.id, a.title, a.start_time, a.end_time, h.hours, h.remark
|
||||
FROM hours h
|
||||
@@ -52,18 +85,40 @@ router.get('/:id', (req: Request, res: Response) => {
|
||||
res.json({ ...user, activities });
|
||||
});
|
||||
|
||||
function bindUserGroups(db: any, userId: number, groupIds: number[] | undefined) {
|
||||
db.prepareCompat('DELETE FROM user_groups WHERE user_id = ?').run(userId);
|
||||
const insertUG = db.prepareCompat('INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?,?)');
|
||||
if (Array.isArray(groupIds)) {
|
||||
const seen = new Set<number>();
|
||||
for (const gid of groupIds) {
|
||||
const n = Number(gid);
|
||||
if (!isNaN(n) && n > 0 && !seen.has(n)) {
|
||||
seen.add(n);
|
||||
insertUG.run(userId, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/', authMiddleware, (req: AuthRequest, res: Response) => {
|
||||
const { class: cls, name, volunteer_no, group_id } = req.body || {};
|
||||
const { class: cls, name, volunteer_no, group_id, group_ids } = req.body || {};
|
||||
if (!cls || !name || !String(cls).trim() || !String(name).trim()) {
|
||||
return res.status(400).json({ error: '班级和姓名不能为空' });
|
||||
}
|
||||
const groups = Array.isArray(group_ids) ? group_ids : (group_id ? [group_id] : []);
|
||||
const db = getDb();
|
||||
try {
|
||||
db.execCompat('BEGIN');
|
||||
const firstGroup = groups[0] ?? null;
|
||||
const info = db.prepareCompat('INSERT INTO users (class, name, volunteer_no, group_id) VALUES (?,?,?,?)')
|
||||
.run(String(cls).trim(), String(name).trim(), volunteer_no || null, group_id || null);
|
||||
.run(String(cls).trim(), String(name).trim(), volunteer_no || null, firstGroup);
|
||||
const userId = Number(info.lastInsertRowid);
|
||||
bindUserGroups(db, userId, groups);
|
||||
db.execCompat('COMMIT');
|
||||
db.save();
|
||||
res.json({ id: info.lastInsertRowid });
|
||||
res.json({ id: userId });
|
||||
} catch (e: any) {
|
||||
try { db.execCompat('ROLLBACK'); } catch {}
|
||||
if (String(e.message).includes('UNIQUE')) {
|
||||
return res.status(400).json({ error: '该班级下已存在同名部员' });
|
||||
}
|
||||
@@ -73,17 +128,23 @@ router.post('/', authMiddleware, (req: AuthRequest, res: Response) => {
|
||||
|
||||
router.put('/:id', authMiddleware, (req: AuthRequest, res: Response) => {
|
||||
const id = Number(req.params.id);
|
||||
const { class: cls, name, volunteer_no, group_id } = req.body || {};
|
||||
const { class: cls, name, volunteer_no, group_id, group_ids } = req.body || {};
|
||||
if (!cls || !name || !String(cls).trim() || !String(name).trim()) {
|
||||
return res.status(400).json({ error: '班级和姓名不能为空' });
|
||||
}
|
||||
const groups = Array.isArray(group_ids) ? group_ids : (group_id ? [group_id] : []);
|
||||
const db = getDb();
|
||||
try {
|
||||
db.execCompat('BEGIN');
|
||||
const firstGroup = groups[0] ?? null;
|
||||
db.prepareCompat('UPDATE users SET class=?, name=?, volunteer_no=?, group_id=? WHERE id=?')
|
||||
.run(String(cls).trim(), String(name).trim(), volunteer_no || null, group_id || null, id);
|
||||
.run(String(cls).trim(), String(name).trim(), volunteer_no || null, firstGroup, id);
|
||||
bindUserGroups(db, id, groups);
|
||||
db.execCompat('COMMIT');
|
||||
db.save();
|
||||
res.json({ ok: true });
|
||||
} catch (e: any) {
|
||||
try { db.execCompat('ROLLBACK'); } catch {}
|
||||
if (String(e.message).includes('UNIQUE')) {
|
||||
return res.status(400).json({ error: '该班级下已存在同名部员' });
|
||||
}
|
||||
@@ -99,4 +160,76 @@ router.delete('/:id', authMiddleware, (req: AuthRequest, res: Response) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
interface BatchItem {
|
||||
class: string;
|
||||
name: string;
|
||||
volunteer_no?: string | null;
|
||||
group_ids?: number[];
|
||||
group_id?: number | null;
|
||||
}
|
||||
|
||||
interface BatchResult {
|
||||
total: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
results: Array<{ idx: number; ok: boolean; id?: number; error?: string; item: BatchItem }>;
|
||||
}
|
||||
|
||||
router.post('/batch', authMiddleware, (req: AuthRequest, res: Response) => {
|
||||
const { items } = req.body || {};
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ error: '批量数据不能为空' });
|
||||
}
|
||||
const db = getDb();
|
||||
const results: BatchResult['results'] = [];
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
try {
|
||||
db.execCompat('BEGIN');
|
||||
const insertUser = db.prepareCompat('INSERT INTO users (class, name, volunteer_no, group_id) VALUES (?,?,?,?)');
|
||||
const insertUG = db.prepareCompat('INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?,?)');
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const raw = items[i] || {};
|
||||
const cls = String(raw.class || '').trim();
|
||||
const name = String(raw.name || '').trim();
|
||||
const volunteer_no = raw.volunteer_no ? String(raw.volunteer_no).trim() || null : null;
|
||||
if (!cls || !name) {
|
||||
failed++;
|
||||
results.push({ idx: i, ok: false, error: '班级和姓名不能为空', item: raw });
|
||||
continue;
|
||||
}
|
||||
const groups: number[] = [];
|
||||
if (Array.isArray(raw.group_ids)) {
|
||||
for (const g of raw.group_ids) {
|
||||
const n = Number(g);
|
||||
if (!isNaN(n) && n > 0 && !groups.includes(n)) groups.push(n);
|
||||
}
|
||||
} else if (raw.group_id) {
|
||||
const n = Number(raw.group_id);
|
||||
if (!isNaN(n) && n > 0) groups.push(n);
|
||||
}
|
||||
const firstGroup = groups[0] ?? null;
|
||||
try {
|
||||
const info = insertUser.run(cls, name, volunteer_no, firstGroup);
|
||||
const userId = Number(info.lastInsertRowid);
|
||||
for (const gid of groups) insertUG.run(userId, gid);
|
||||
success++;
|
||||
results.push({ idx: i, ok: true, id: userId, item: raw });
|
||||
} catch (e: any) {
|
||||
failed++;
|
||||
let err = e?.message || '添加失败';
|
||||
if (String(err).includes('UNIQUE')) err = '该班级下已存在同名部员';
|
||||
results.push({ idx: i, ok: false, error: err, item: raw });
|
||||
}
|
||||
}
|
||||
db.execCompat('COMMIT');
|
||||
db.save();
|
||||
res.json({ total: items.length, success, failed, results } as BatchResult);
|
||||
} catch (e: any) {
|
||||
try { db.execCompat('ROLLBACK'); } catch {}
|
||||
res.status(500).json({ error: e?.message || '批量处理失败' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user