@dalehkx/quote-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/quote.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { registerInquiryCommands } from '../src/commands/inquiry.mjs';
5
+ import { registerReplyCommands } from '../src/commands/reply.mjs';
6
+ import { registerCompareCommand } from '../src/commands/compare.mjs';
7
+ import { registerOrderCommands } from '../src/commands/order.mjs';
8
+ import { registerConfigCommands } from '../src/commands/config.mjs';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('quote')
14
+ .description('通用询报价 CLI 工具')
15
+ .version('0.1.0');
16
+
17
+ registerInquiryCommands(program);
18
+ registerReplyCommands(program);
19
+ registerCompareCommand(program);
20
+ registerOrderCommands(program);
21
+ registerConfigCommands(program);
22
+
23
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@dalehkx/quote-cli",
3
+ "version": "0.1.0",
4
+ "description": "通用询报价 CLI 工具",
5
+ "type": "module",
6
+ "bin": {
7
+ "quote": "bin/quote.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/"
12
+ ],
13
+ "scripts": {
14
+ "start": "node bin/quote.js"
15
+ },
16
+ "keywords": [
17
+ "quote",
18
+ "rfq",
19
+ "inquiry",
20
+ "procurement",
21
+ "cli"
22
+ ],
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "commander": "^12.1.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ }
33
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * API 远程接口适配器(骨架)
3
+ * 后续对接真实后端时在此实现 HTTP 调用
4
+ */
5
+ export class ApiAdapter {
6
+ constructor(config = {}) {
7
+ this.baseUrl = config.apiBase || '';
8
+ this.token = config.apiToken || '';
9
+ }
10
+
11
+ _headers() {
12
+ const h = { 'Content-Type': 'application/json' };
13
+ if (this.token) h['Authorization'] = `Bearer ${this.token}`;
14
+ return h;
15
+ }
16
+
17
+ async _request(method, path, body) {
18
+ if (!this.baseUrl) {
19
+ throw new Error('API 模式未配置 apiBase,请执行: quote config set --mode api --api-base <url>');
20
+ }
21
+ const url = `${this.baseUrl}${path}`;
22
+ const opts = { method, headers: this._headers() };
23
+ if (body) opts.body = JSON.stringify(body);
24
+
25
+ const res = await fetch(url, opts);
26
+ if (!res.ok) {
27
+ const text = await res.text();
28
+ throw new Error(`API 请求失败 [${res.status}]: ${text}`);
29
+ }
30
+ return res.json();
31
+ }
32
+
33
+ // ─── Inquiry ─────────────────────────────────────────────
34
+
35
+ async createInquiry(data) {
36
+ return this._request('POST', '/inquiries', data);
37
+ }
38
+
39
+ async listInquiries(filter = {}) {
40
+ const params = new URLSearchParams(filter).toString();
41
+ return this._request('GET', `/inquiries${params ? '?' + params : ''}`);
42
+ }
43
+
44
+ async getInquiry(id) {
45
+ return this._request('GET', `/inquiries/${id}`);
46
+ }
47
+
48
+ async closeInquiry(id) {
49
+ return this._request('PATCH', `/inquiries/${id}`, { status: 'closed' });
50
+ }
51
+
52
+ // ─── Reply ───────────────────────────────────────────────
53
+
54
+ async createReply(data) {
55
+ return this._request('POST', '/replies', data);
56
+ }
57
+
58
+ async listReplies(inquiryId) {
59
+ return this._request('GET', `/inquiries/${inquiryId}/replies`);
60
+ }
61
+
62
+ // ─── Compare ─────────────────────────────────────────────
63
+
64
+ async compareReplies(inquiryId, sort = 'price') {
65
+ return this._request('GET', `/inquiries/${inquiryId}/compare?sort=${sort}`);
66
+ }
67
+
68
+ // ─── Order ───────────────────────────────────────────────
69
+
70
+ async confirmOrder(inquiryId, replyId) {
71
+ return this._request('POST', '/orders', { inquiryId, replyId });
72
+ }
73
+
74
+ async listOrders() {
75
+ return this._request('GET', '/orders');
76
+ }
77
+
78
+ // ─── Config ──────────────────────────────────────────────
79
+ // config 始终本地存储(包含 apiBase 等连接信息)
80
+
81
+ async getConfig() {
82
+ // 由 index.mjs 在创建 adapter 前已读取,这里直接返回
83
+ const { Store } = await import('../store.mjs');
84
+ const store = new Store();
85
+ return store.getConfig();
86
+ }
87
+
88
+ async setConfig(updates) {
89
+ const { Store } = await import('../store.mjs');
90
+ const store = new Store();
91
+ return store.setConfig(updates);
92
+ }
93
+ }
@@ -0,0 +1,24 @@
1
+ import { Store } from '../store.mjs';
2
+ import { LocalAdapter } from './local.mjs';
3
+ import { ApiAdapter } from './api.mjs';
4
+
5
+ let _adapter = null;
6
+
7
+ /**
8
+ * 获取数据适配器(单例)
9
+ * 根据 config.mode 决定使用本地存储还是远程 API
10
+ */
11
+ export function getAdapter() {
12
+ if (_adapter) return _adapter;
13
+
14
+ const store = new Store();
15
+ const config = store.getConfig();
16
+
17
+ if (config.mode === 'api') {
18
+ _adapter = new ApiAdapter(config);
19
+ } else {
20
+ _adapter = new LocalAdapter();
21
+ }
22
+
23
+ return _adapter;
24
+ }
@@ -0,0 +1,125 @@
1
+ import { Store } from '../store.mjs';
2
+ import { createInquiry, createReply, createOrder } from '../models.mjs';
3
+
4
+ /**
5
+ * 本地 JSON 文件存储适配器
6
+ * 将 Store 的底层操作包装为统一的业务接口
7
+ */
8
+ export class LocalAdapter {
9
+ constructor() {
10
+ this.store = new Store();
11
+ }
12
+
13
+ // ─── Inquiry ─────────────────────────────────────────────
14
+
15
+ async createInquiry(data) {
16
+ const config = this.store.getConfig();
17
+ const id = this.store.nextId('INQ');
18
+ const record = { id, ...createInquiry(data, config.name) };
19
+ return this.store.save('inquiries', record);
20
+ }
21
+
22
+ async listInquiries(filter = {}) {
23
+ return this.store.list('inquiries', (item) => {
24
+ if (filter.status && item.status !== filter.status) return false;
25
+ return true;
26
+ });
27
+ }
28
+
29
+ async getInquiry(id) {
30
+ return this.store.get('inquiries', id);
31
+ }
32
+
33
+ async closeInquiry(id) {
34
+ const record = this.store.get('inquiries', id);
35
+ if (!record) return null;
36
+ record.status = 'closed';
37
+ return this.store.save('inquiries', record);
38
+ }
39
+
40
+ // ─── Reply ───────────────────────────────────────────────
41
+
42
+ async createReply(data) {
43
+ const inquiry = this.store.get('inquiries', data.inquiryId);
44
+ if (!inquiry) return null;
45
+
46
+ const id = this.store.nextId('QUO');
47
+ const record = { id, ...createReply(data) };
48
+ this.store.save('replies', record);
49
+
50
+ // 更新询价单状态
51
+ if (inquiry.status === 'pending') {
52
+ inquiry.status = 'quoted';
53
+ this.store.save('inquiries', inquiry);
54
+ }
55
+
56
+ return record;
57
+ }
58
+
59
+ async listReplies(inquiryId) {
60
+ return this.store.list('replies', r => r.inquiryId === inquiryId);
61
+ }
62
+
63
+ // ─── Compare ─────────────────────────────────────────────
64
+
65
+ async compareReplies(inquiryId, sort = 'price') {
66
+ const inquiry = this.store.get('inquiries', inquiryId);
67
+ if (!inquiry) return null;
68
+
69
+ const replies = this.store.list('replies', r => r.inquiryId === inquiryId);
70
+ if (replies.length === 0) return { inquiry, sorted: [], lowest: null, fastest: null };
71
+
72
+ const sorted = [...replies].sort((a, b) => {
73
+ switch (sort) {
74
+ case 'price': return a.price - b.price;
75
+ case 'delivery': return (a.delivery || 999) - (b.delivery || 999);
76
+ case 'brand': return (a.brand || '').localeCompare(b.brand || '');
77
+ default: return a.price - b.price;
78
+ }
79
+ });
80
+
81
+ const lowest = sorted[0];
82
+ const fastest = [...replies].sort((a, b) => (a.delivery || 999) - (b.delivery || 999))[0];
83
+
84
+ return { inquiry, sorted, lowest, fastest };
85
+ }
86
+
87
+ // ─── Order ───────────────────────────────────────────────
88
+
89
+ async confirmOrder(inquiryId, replyId) {
90
+ const inquiry = this.store.get('inquiries', inquiryId);
91
+ if (!inquiry) return null;
92
+
93
+ const reply = this.store.get('replies', replyId);
94
+ if (!reply) return null;
95
+ if (reply.inquiryId !== inquiryId) return null;
96
+
97
+ const id = this.store.nextId('ORD');
98
+ const record = { id, ...createOrder({ inquiryId, replyId }) };
99
+ this.store.save('orders', record);
100
+
101
+ inquiry.status = 'ordered';
102
+ this.store.save('inquiries', inquiry);
103
+
104
+ return { order: record, inquiry, reply };
105
+ }
106
+
107
+ async listOrders() {
108
+ const orders = this.store.list('orders');
109
+ return orders.map(order => ({
110
+ ...order,
111
+ inquiry: this.store.get('inquiries', order.inquiryId),
112
+ reply: this.store.get('replies', order.replyId),
113
+ }));
114
+ }
115
+
116
+ // ─── Config ──────────────────────────────────────────────
117
+
118
+ async getConfig() {
119
+ return this.store.getConfig();
120
+ }
121
+
122
+ async setConfig(updates) {
123
+ return this.store.setConfig(updates);
124
+ }
125
+ }
@@ -0,0 +1,46 @@
1
+ import { getAdapter } from '../adapter/index.mjs';
2
+
3
+ export function registerCompareCommand(program) {
4
+ program
5
+ .command('compare')
6
+ .description('比价分析(对某询价单的所有报价进行对比)')
7
+ .requiredOption('-i, --inquiry <id>', '询价单 ID')
8
+ .option('-s, --sort <field>', '排序字段 (price|delivery|brand)', 'price')
9
+ .action(async (opts) => {
10
+ const adapter = getAdapter();
11
+ const result = await adapter.compareReplies(opts.inquiry, opts.sort);
12
+
13
+ if (!result) {
14
+ console.error(`未找到询价单: ${opts.inquiry}`);
15
+ process.exit(1);
16
+ }
17
+
18
+ const { inquiry, sorted, lowest, fastest } = result;
19
+
20
+ if (sorted.length === 0) {
21
+ console.log('该询价单暂无报价,无法比较');
22
+ return;
23
+ }
24
+
25
+ console.log(`\n询价: ${inquiry.product} (${inquiry.id})`);
26
+ console.log(`车型: ${inquiry.vehicle || '-'} | OE: ${inquiry.oeNumber || '-'} | 数量: ${inquiry.quantity}`);
27
+ console.log(`排序: ${opts.sort}\n`);
28
+ console.log('─'.repeat(80));
29
+ console.log(`${'排名'.padEnd(4)} | ${'供应商'.padEnd(12)} | ${'价格'.padEnd(10)} | ${'品牌'.padEnd(10)} | ${'货期'.padEnd(6)} | 备注`);
30
+ console.log('─'.repeat(80));
31
+
32
+ sorted.forEach((r, i) => {
33
+ const rank = `#${i + 1}`.padEnd(4);
34
+ const supplier = (r.supplier || '-').padEnd(12).slice(0, 12);
35
+ const price = `${r.currency} ${r.price}`.padEnd(10);
36
+ const brand = (r.brand || '-').padEnd(10).slice(0, 10);
37
+ const delivery = r.delivery ? `${r.delivery}天`.padEnd(6) : '未知'.padEnd(6);
38
+ console.log(`${rank} | ${supplier} | ${price} | ${brand} | ${delivery} | ${r.note || ''}`);
39
+ });
40
+
41
+ console.log('─'.repeat(80));
42
+
43
+ if (lowest) console.log(`\n💰 最低价: ${lowest.supplier} — ${lowest.currency} ${lowest.price}`);
44
+ if (fastest && fastest.delivery) console.log(`🚚 最快货期: ${fastest.supplier} — ${fastest.delivery}天`);
45
+ });
46
+ }
@@ -0,0 +1,49 @@
1
+ import { getAdapter } from '../adapter/index.mjs';
2
+
3
+ export function registerConfigCommands(program) {
4
+ const config = program.command('config').description('配置管理');
5
+
6
+ config
7
+ .command('set')
8
+ .description('设置配置项')
9
+ .option('-r, --role <role>', '角色 (buyer|supplier)')
10
+ .option('-n, --name <name>', '姓名/联系人')
11
+ .option('-c, --company <company>', '公司名称')
12
+ .option('--phone <phone>', '联系电话')
13
+ .option('-m, --mode <mode>', '数据模式 (local|api)')
14
+ .option('--api-base <url>', 'API 基础地址 (api 模式)')
15
+ .option('--api-token <token>', 'API Token (api 模式)')
16
+ .action(async (opts) => {
17
+ const adapter = getAdapter();
18
+ const updates = {};
19
+ if (opts.role) updates.role = opts.role;
20
+ if (opts.name) updates.name = opts.name;
21
+ if (opts.company) updates.company = opts.company;
22
+ if (opts.phone) updates.phone = opts.phone;
23
+ if (opts.mode) updates.mode = opts.mode;
24
+ if (opts.apiBase) updates.apiBase = opts.apiBase;
25
+ if (opts.apiToken) updates.apiToken = opts.apiToken;
26
+
27
+ if (Object.keys(updates).length === 0) {
28
+ console.error('请至少指定一个配置项');
29
+ process.exit(1);
30
+ }
31
+
32
+ const result = await adapter.setConfig(updates);
33
+ console.log('✓ 配置已更新:');
34
+ console.log(JSON.stringify(result, null, 2));
35
+ });
36
+
37
+ config
38
+ .command('show')
39
+ .description('查看当前配置')
40
+ .action(async () => {
41
+ const adapter = getAdapter();
42
+ const cfg = await adapter.getConfig();
43
+ if (Object.keys(cfg).length === 0) {
44
+ console.log('暂无配置,使用 quote config set 进行设置');
45
+ return;
46
+ }
47
+ console.log(JSON.stringify(cfg, null, 2));
48
+ });
49
+ }
@@ -0,0 +1,80 @@
1
+ import { getAdapter } from '../adapter/index.mjs';
2
+
3
+ export function registerInquiryCommands(program) {
4
+ const inquiry = program.command('inquiry').description('询价单管理');
5
+
6
+ inquiry
7
+ .command('create')
8
+ .description('创建询价单')
9
+ .requiredOption('-p, --product <name>', '零件/产品名称')
10
+ .option('-o, --oe <number>', 'OE 编号')
11
+ .option('-v, --vehicle <model>', '车型/适用型号')
12
+ .option('-q, --quantity <n>', '数量', '1')
13
+ .option('-n, --note <text>', '备注')
14
+ .action(async (opts) => {
15
+ const adapter = getAdapter();
16
+ const record = await adapter.createInquiry({
17
+ product: opts.product,
18
+ oeNumber: opts.oe || '',
19
+ vehicle: opts.vehicle || '',
20
+ quantity: opts.quantity,
21
+ note: opts.note || '',
22
+ });
23
+ console.log(`✓ 询价单已创建: ${record.id}`);
24
+ console.log(JSON.stringify(record, null, 2));
25
+ });
26
+
27
+ inquiry
28
+ .command('list')
29
+ .description('查看询价单列表')
30
+ .option('-s, --status <status>', '按状态筛选 (pending|quoted|ordered|closed)')
31
+ .action(async (opts) => {
32
+ const adapter = getAdapter();
33
+ const items = await adapter.listInquiries({ status: opts.status });
34
+ if (items.length === 0) {
35
+ console.log('暂无询价单');
36
+ return;
37
+ }
38
+ console.log(`共 ${items.length} 条询价单:\n`);
39
+ for (const item of items) {
40
+ const replies = await adapter.listReplies(item.id);
41
+ console.log(` ${item.id} | ${item.product} | 数量:${item.quantity} | 状态:${item.status} | 报价:${replies.length}条`);
42
+ }
43
+ });
44
+
45
+ inquiry
46
+ .command('detail')
47
+ .description('查看询价单详情')
48
+ .argument('<id>', '询价单 ID')
49
+ .action(async (id) => {
50
+ const adapter = getAdapter();
51
+ const record = await adapter.getInquiry(id);
52
+ if (!record) {
53
+ console.error(`未找到询价单: ${id}`);
54
+ process.exit(1);
55
+ }
56
+ console.log(JSON.stringify(record, null, 2));
57
+
58
+ const replies = await adapter.listReplies(id);
59
+ if (replies.length > 0) {
60
+ console.log(`\n关联报价 (${replies.length} 条):`);
61
+ for (const r of replies) {
62
+ console.log(` ${r.id} | ${r.supplier} | ¥${r.price} | ${r.brand} | ${r.delivery || '?'}天`);
63
+ }
64
+ }
65
+ });
66
+
67
+ inquiry
68
+ .command('close')
69
+ .description('关闭询价单')
70
+ .argument('<id>', '询价单 ID')
71
+ .action(async (id) => {
72
+ const adapter = getAdapter();
73
+ const record = await adapter.closeInquiry(id);
74
+ if (!record) {
75
+ console.error(`未找到询价单: ${id}`);
76
+ process.exit(1);
77
+ }
78
+ console.log(`✓ 询价单已关闭: ${id}`);
79
+ });
80
+ }
@@ -0,0 +1,45 @@
1
+ import { getAdapter } from '../adapter/index.mjs';
2
+
3
+ export function registerOrderCommands(program) {
4
+ const order = program.command('order').description('订单管理');
5
+
6
+ order
7
+ .command('confirm')
8
+ .description('确认下单(选择某个报价下单)')
9
+ .requiredOption('-i, --inquiry <id>', '询价单 ID')
10
+ .requiredOption('-r, --reply <id>', '报价 ID')
11
+ .action(async (opts) => {
12
+ const adapter = getAdapter();
13
+ const result = await adapter.confirmOrder(opts.inquiry, opts.reply);
14
+
15
+ if (!result) {
16
+ console.error(`下单失败:请检查询价单 ${opts.inquiry} 和报价 ${opts.reply} 是否存在且匹配`);
17
+ process.exit(1);
18
+ }
19
+
20
+ const { order: record, inquiry, reply } = result;
21
+ console.log(`✓ 订单已确认: ${record.id}`);
22
+ console.log(` 询价: ${inquiry.product} (${inquiry.id})`);
23
+ console.log(` 供应商: ${reply.supplier} | 价格: ${reply.currency} ${reply.price}`);
24
+ console.log(JSON.stringify(record, null, 2));
25
+ });
26
+
27
+ order
28
+ .command('list')
29
+ .description('查看所有订单')
30
+ .action(async () => {
31
+ const adapter = getAdapter();
32
+ const items = await adapter.listOrders();
33
+ if (items.length === 0) {
34
+ console.log('暂无订单');
35
+ return;
36
+ }
37
+ console.log(`共 ${items.length} 条订单:\n`);
38
+ for (const item of items) {
39
+ const product = item.inquiry ? item.inquiry.product : '?';
40
+ const supplier = item.reply ? item.reply.supplier : '?';
41
+ const price = item.reply ? `${item.reply.currency} ${item.reply.price}` : '?';
42
+ console.log(` ${item.id} | ${product} | ${supplier} | ${price} | ${item.confirmedAt}`);
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,51 @@
1
+ import { getAdapter } from '../adapter/index.mjs';
2
+
3
+ export function registerReplyCommands(program) {
4
+ const reply = program.command('reply').description('报价管理');
5
+
6
+ reply
7
+ .command('create')
8
+ .description('创建报价(对某询价单报价)')
9
+ .requiredOption('-i, --inquiry <id>', '询价单 ID')
10
+ .requiredOption('--price <amount>', '报价金额')
11
+ .option('-s, --supplier <name>', '供应商名称')
12
+ .option('-b, --brand <brand>', '品牌')
13
+ .option('-d, --delivery <days>', '货期(天)')
14
+ .option('-c, --currency <code>', '货币', 'CNY')
15
+ .option('-n, --note <text>', '备注')
16
+ .action(async (opts) => {
17
+ const adapter = getAdapter();
18
+ const record = await adapter.createReply({
19
+ inquiryId: opts.inquiry,
20
+ supplier: opts.supplier,
21
+ price: opts.price,
22
+ currency: opts.currency,
23
+ brand: opts.brand,
24
+ delivery: opts.delivery,
25
+ note: opts.note,
26
+ });
27
+ if (!record) {
28
+ console.error(`未找到询价单: ${opts.inquiry}`);
29
+ process.exit(1);
30
+ }
31
+ console.log(`✓ 报价已创建: ${record.id}`);
32
+ console.log(JSON.stringify(record, null, 2));
33
+ });
34
+
35
+ reply
36
+ .command('list')
37
+ .description('查看某询价单的所有报价')
38
+ .requiredOption('-i, --inquiry <id>', '询价单 ID')
39
+ .action(async (opts) => {
40
+ const adapter = getAdapter();
41
+ const items = await adapter.listReplies(opts.inquiry);
42
+ if (items.length === 0) {
43
+ console.log(`询价单 ${opts.inquiry} 暂无报价`);
44
+ return;
45
+ }
46
+ console.log(`询价单 ${opts.inquiry} 共 ${items.length} 条报价:\n`);
47
+ for (const item of items) {
48
+ console.log(` ${item.id} | ${item.supplier} | ${item.currency} ${item.price} | ${item.brand || '-'} | ${item.delivery || '?'}天 | ${item.note || ''}`);
49
+ }
50
+ });
51
+ }
package/src/models.mjs ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 数据模型定义与校验
3
+ */
4
+
5
+ export function createInquiry({ product, oeNumber, vehicle, quantity, note }, createdBy) {
6
+ if (!product) throw new Error('product 是必填项');
7
+ return {
8
+ product,
9
+ oeNumber: oeNumber || '',
10
+ vehicle: vehicle || '',
11
+ quantity: quantity ? Number(quantity) : 1,
12
+ note: note || '',
13
+ status: 'pending', // pending | quoted | ordered | closed
14
+ createdAt: new Date().toISOString(),
15
+ createdBy: createdBy || 'anonymous',
16
+ };
17
+ }
18
+
19
+ export function createReply({ inquiryId, supplier, price, currency, brand, delivery, note }) {
20
+ if (!inquiryId) throw new Error('inquiryId 是必填项');
21
+ if (!price) throw new Error('price 是必填项');
22
+ return {
23
+ inquiryId,
24
+ supplier: supplier || 'unknown',
25
+ price: Number(price),
26
+ currency: currency || 'CNY',
27
+ brand: brand || '',
28
+ delivery: delivery ? Number(delivery) : null, // 货期(天)
29
+ note: note || '',
30
+ createdAt: new Date().toISOString(),
31
+ };
32
+ }
33
+
34
+ export function createOrder({ inquiryId, replyId }) {
35
+ if (!inquiryId) throw new Error('inquiryId 是必填项');
36
+ if (!replyId) throw new Error('replyId 是必填项');
37
+ return {
38
+ inquiryId,
39
+ replyId,
40
+ status: 'confirmed',
41
+ confirmedAt: new Date().toISOString(),
42
+ };
43
+ }
package/src/store.mjs ADDED
@@ -0,0 +1,88 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * 本地 JSON 文件存储层
6
+ * 数据存放在 cwd/.quote-data/ 下
7
+ */
8
+ export class Store {
9
+ constructor(baseDir = process.cwd()) {
10
+ this.root = join(baseDir, '.quote-data');
11
+ this.dirs = {
12
+ inquiries: join(this.root, 'inquiries'),
13
+ replies: join(this.root, 'replies'),
14
+ orders: join(this.root, 'orders'),
15
+ };
16
+ this.configPath = join(this.root, 'config.json');
17
+ }
18
+
19
+ /** 确保数据目录存在 */
20
+ init() {
21
+ for (const dir of Object.values(this.dirs)) {
22
+ if (!existsSync(dir)) {
23
+ mkdirSync(dir, { recursive: true });
24
+ }
25
+ }
26
+ }
27
+
28
+ // ─── 通用 CRUD ───────────────────────────────────────────
29
+
30
+ _filePath(collection, id) {
31
+ return join(this.dirs[collection], `${id}.json`);
32
+ }
33
+
34
+ save(collection, record) {
35
+ this.init();
36
+ const filePath = this._filePath(collection, record.id);
37
+ writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8');
38
+ return record;
39
+ }
40
+
41
+ get(collection, id) {
42
+ const filePath = this._filePath(collection, id);
43
+ if (!existsSync(filePath)) return null;
44
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
45
+ }
46
+
47
+ list(collection, filterFn = () => true) {
48
+ this.init();
49
+ const dir = this.dirs[collection];
50
+ if (!existsSync(dir)) return [];
51
+ return readdirSync(dir)
52
+ .filter(f => f.endsWith('.json'))
53
+ .map(f => JSON.parse(readFileSync(join(dir, f), 'utf-8')))
54
+ .filter(filterFn)
55
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
56
+ }
57
+
58
+ // ─── Config ──────────────────────────────────────────────
59
+
60
+ getConfig() {
61
+ this.init();
62
+ if (!existsSync(this.configPath)) return {};
63
+ return JSON.parse(readFileSync(this.configPath, 'utf-8'));
64
+ }
65
+
66
+ setConfig(updates) {
67
+ this.init();
68
+ const config = { ...this.getConfig(), ...updates };
69
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
70
+ return config;
71
+ }
72
+
73
+ // ─── ID 生成 ─────────────────────────────────────────────
74
+
75
+ nextId(prefix) {
76
+ const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
77
+ const existing = readdirSync(this.dirs[this._collectionForPrefix(prefix)] || this.root)
78
+ .filter(f => f.startsWith(prefix))
79
+ .length;
80
+ const seq = String(existing + 1).padStart(3, '0');
81
+ return `${prefix}-${date}-${seq}`;
82
+ }
83
+
84
+ _collectionForPrefix(prefix) {
85
+ const map = { INQ: 'inquiries', QUO: 'replies', ORD: 'orders' };
86
+ return map[prefix] || 'inquiries';
87
+ }
88
+ }