Files
i/server/src/services/grocy.js
T
wsh5485 65b0bb04f8 feat: import CarLog v2.8 code + dev plan
把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline:
- server/src/ (13 个路由 + middleware + services + config)
- server/migrations/ (0001~0018 共 18 个迁移 + mysql)
- server/test/ (12 文件 101 测试)
- client/src/ (20 个 view + components + stores + api + composables)
- client/public/ + client/scripts/
- 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql)
- .husky/pre-commit (git hooks)
- docs/install/ (宝塔部署文档)

不含:
- node_modules/ (本地 npm install)
- .env (敏感, 走 .env.example)
- *.zip / *.log / *.sqlite / .DS_Store

新增文档 docs/DEV-PLAN.md:
- Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view)
- Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单)
- Phase 3: 验证 (测试 + E2E + DB 完整性)
- 交付清单 + commit 模板 + 给 Mavis review 的材料

后续 Trae 实施, 提交后我 code review + 跑测试。
2026-06-20 22:30:19 +08:00

87 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/src/services/grocy.js — Grocy 库存扣减同步
import { httpGet, httpPost } from '../http.js';
import { db } from '../db.js';
/**
* 同步 chemical_usage 表到 Grocy 库存扣减
* @param {object} cfg
* @param {object} opts
* @param {string} [opts.since] YYYY-MM-DD,只同步此日期之后的
* @param {boolean} [opts.dryRun]
* @returns {Promise<{ok:number, fail:number, items:Array}>}
*/
export async function syncUsageToGrocy(cfg, opts = {}) {
if (!cfg.grocy.url) throw new Error('未配置 GROCY_URL');
if (!cfg.grocy.api_token) throw new Error('未配置 GROCY_API_TOKEN');
const since = opts.since || isoDaysAgo(7);
const rows = await db().all(
`SELECT cu.id, cu.usage_date, cu.chemical_id, cu.amount, c.name
FROM chemical_usage cu
LEFT JOIN chemicals c ON c.grocy_product_id = cu.chemical_id
WHERE cu.sync_status = 'pending' AND cu.usage_date >= ?
ORDER BY cu.usage_date, cu.id`, [since]);
// 按 product 聚合
const agg = {};
for (const r of rows) {
const k = r.chemical_id;
if (!agg[k]) agg[k] = { product_id: k, name: r.name, total: 0, ids: [] };
agg[k].total += r.amount;
agg[k].ids.push(r.id);
}
const results = [];
for (const a of Object.values(agg)) {
const url = `${cfg.grocy.url}/api/stock/products/${encodeURIComponent(a.product_id)}/consume`;
const amount = Math.round(a.total * 100) / 100; // 保留 2 位小数
if (opts.dryRun) {
results.push({ product_id: a.product_id, name: a.name, amount, status: 'dry-run' });
continue;
}
try {
await httpPost(url, { amount }, {
headers: { 'GROCY-API-TOKEN': cfg.grocy.api_token, 'Content-Type': 'application/json' },
timeout: 10000,
});
// 标记 synced
const placeholders = a.ids.map(() => '?').join(',');
(await db().run(`UPDATE chemical_usage SET sync_status = 'synced', sync_at = NOW(), updated_at = NOW() WHERE id IN (${placeholders})`, [...a.ids]));
results.push({ product_id: a.product_id, name: a.name, amount, status: 'ok' });
} catch (e) {
(await db().run(`UPDATE chemical_usage SET sync_status = 'failed', updated_at = NOW() WHERE id IN (${a.ids.map(() => '?').join(',')})`, [...a.ids]));
results.push({ product_id: a.product_id, name: a.name, amount, status: 'fail', error: e.message });
}
}
return { ok: results.filter(r => r.status === 'ok').length, fail: results.filter(r => r.status === 'fail').length, items: results };
}
export async function cli(argv, cfg) {
if (argv.includes('--help') || argv.includes('-h')) {
console.log(`Usage: grocy-sync [--since=YYYY-MM-DD] [--dry-run]
把 chemical_usage 表中 sync_status=pending 的记录聚合到 Grocy 库存扣减。`);
return;
}
const args = parseArgs(argv);
cfg = cfg || (await import('../config.js')).loadConfig();
if (!cfg.grocy.url || !cfg.grocy.api_token) {
console.log('✗ 未配置 GROCY_URL / GROCY_API_TOKEN(设置 → Grocy');
process.exit(78);
}
const r = await syncUsageToGrocy(cfg, { since: args.since, dryRun: args['dry-run'] });
console.log(`✓ Grocy sync: ok=${r.ok} fail=${r.fail}`);
for (const it of r.items) {
console.log(` [${it.status}] ${it.product_id} ${it.name} ${it.amount}`);
}
}
function parseArgs(argv) {
const a = {};
for (const x of argv) {
const m = x.match(/^--([^=]+)(?:=(.*))?$/);
if (m) a[m[1]] = m[2] ?? true;
}
return a;
}
function isoDaysAgo(d) { return new Date(Date.now() - d * 86400 * 1000).toISOString().slice(0, 10); }