|
|
@@ -139,23 +139,31 @@ function register(app, ctx) {
|
|
|
if (!(await requireHealthy(store, res))) return;
|
|
|
|
|
|
const sha256 = crypto.createHash('sha256').update(content).digest('hex');
|
|
|
+ const existing = await store.findUploadBySha(sha256);
|
|
|
if (!body.force) {
|
|
|
- const existing = await store.findUploadBySha(sha256);
|
|
|
if (existing) {
|
|
|
return sendJson(res, 409, { error: '该文件已导入过', uploadId: existing, duplicate: true });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const year = Number(body.year) || new Date().getFullYear();
|
|
|
+ const school = String(body.school || '').trim().slice(0, 60);
|
|
|
let parsed;
|
|
|
try {
|
|
|
- parsed = opsImport.parseWorkbook(content, { year });
|
|
|
+ parsed = opsImport.parseWorkbook(content, { year, school });
|
|
|
} catch (e) {
|
|
|
return sendJson(res, 400, { error: e.message || '解析失败', code: e.code || 'OPS_PARSE_FAILED' });
|
|
|
}
|
|
|
if (!parsed.rows.length) {
|
|
|
return sendJson(res, 400, { error: '未解析到有效数据行' });
|
|
|
}
|
|
|
+ // 表格里没有「学校名称」列、上传时也没指定 → 让用户补选,避免落成空白分组
|
|
|
+ if (!parsed.shops.some((shop) => shop.school && shop.school.trim())) {
|
|
|
+ return sendJson(res, 400, {
|
|
|
+ error: '表格里没有「学校名称」列,请在上传时选择或填写学校',
|
|
|
+ code: 'OPS_SCHOOL_REQUIRED',
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
const uploadId = newUploadId();
|
|
|
const uploadedAt = new Date().toISOString();
|
|
|
@@ -177,12 +185,14 @@ function register(app, ctx) {
|
|
|
warningCount: parsed.warnings.length,
|
|
|
dates: { from, to },
|
|
|
year,
|
|
|
+ schoolOverride: school,
|
|
|
parser: { kind: 'manual-ops', version: PARSER_VERSION },
|
|
|
warnings: parsed.warnings.slice(0, 500),
|
|
|
};
|
|
|
|
|
|
try {
|
|
|
await store.withTransaction(async (tx) => {
|
|
|
+ if (body.force) await tx.deleteRowsBySha(sha256);
|
|
|
await tx.insertUpload({
|
|
|
id: uploadId,
|
|
|
originalName: fileName,
|
|
|
@@ -197,6 +207,7 @@ function register(app, ctx) {
|
|
|
});
|
|
|
await tx.upsertRows(parsed.rows.map((row) => ({ ...row, uploadId })));
|
|
|
await tx.upsertShops(parsed.shops.map((shop) => ({ ...shop, lastUploadId: uploadId })));
|
|
|
+ await tx.pruneOrphanShops();
|
|
|
});
|
|
|
} catch (e) {
|
|
|
return sendJson(res, 500, { error: '写入失败', detail: e.message || String(e) });
|
|
|
@@ -297,9 +308,10 @@ function register(app, ctx) {
|
|
|
if (!record) return sendJson(res, 404, { error: '找不到该上传记录' });
|
|
|
const upload = await store.getUpload(params.id);
|
|
|
const year = (upload && upload.year) || new Date().getFullYear();
|
|
|
+ const school = (upload && upload.report && upload.report.schoolOverride) || '';
|
|
|
let parsed;
|
|
|
try {
|
|
|
- parsed = opsImport.parseWorkbook(record.content, { year });
|
|
|
+ parsed = opsImport.parseWorkbook(record.content, { year, school });
|
|
|
} catch (e) {
|
|
|
return sendJson(res, 400, { error: e.message || '解析失败', code: e.code || 'OPS_PARSE_FAILED' });
|
|
|
}
|
|
|
@@ -312,6 +324,7 @@ function register(app, ctx) {
|
|
|
warningCount: parsed.warnings.length,
|
|
|
dates: { from, to },
|
|
|
year,
|
|
|
+ schoolOverride: school,
|
|
|
parser: { kind: 'manual-ops', version: PARSER_VERSION },
|
|
|
warnings: parsed.warnings.slice(0, 500),
|
|
|
reparsedAt: new Date().toISOString(),
|
|
|
@@ -321,6 +334,7 @@ function register(app, ctx) {
|
|
|
await tx.deleteRowsByUpload(params.id);
|
|
|
await tx.upsertRows(parsed.rows.map((row) => ({ ...row, uploadId: params.id })));
|
|
|
await tx.upsertShops(parsed.shops.map((shop) => ({ ...shop, lastUploadId: params.id })));
|
|
|
+ await tx.pruneOrphanShops();
|
|
|
await tx.updateReport(params.id, report);
|
|
|
});
|
|
|
} catch (e) {
|