270 lines
9.5 KiB
JavaScript
270 lines
9.5 KiB
JavaScript
|
|
/**
|
||
|
|
* order-manager.js - 订单核心管理模块
|
||
|
|
* 编号: ZT-YYYYMMDD-NNN, 子任务: -XX
|
||
|
|
* 状态: submitted → reviewing → accepted(开发中) → testing → completed → delivered
|
||
|
|
* 归档: 已完成/已交付订单可移入历史
|
||
|
|
*/
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
import { fileURLToPath } from 'url';
|
||
|
|
|
||
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
|
const DATA_FILE = path.join(__dirname, 'data', 'orders.json');
|
||
|
|
const COUNTER_FILE = path.join(__dirname, 'data', 'counter.json');
|
||
|
|
const HISTORY_FILE = path.join(__dirname, 'data', 'history.json');
|
||
|
|
|
||
|
|
fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });
|
||
|
|
|
||
|
|
function loadOrders() {
|
||
|
|
try { return JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8')); }
|
||
|
|
catch { return []; }
|
||
|
|
}
|
||
|
|
function saveOrders(orders) {
|
||
|
|
fs.writeFileSync(DATA_FILE, JSON.stringify(orders, null, 2));
|
||
|
|
}
|
||
|
|
function loadCounter() {
|
||
|
|
try { return JSON.parse(fs.readFileSync(COUNTER_FILE, 'utf-8')); }
|
||
|
|
catch { return {}; }
|
||
|
|
}
|
||
|
|
function saveCounter(c) {
|
||
|
|
fs.writeFileSync(COUNTER_FILE, JSON.stringify(c, null, 2));
|
||
|
|
}
|
||
|
|
function loadHistory() {
|
||
|
|
try { return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8')); }
|
||
|
|
catch { return []; }
|
||
|
|
}
|
||
|
|
function saveHistory(h) {
|
||
|
|
fs.writeFileSync(HISTORY_FILE, JSON.stringify(h, null, 2));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 生成编号 */
|
||
|
|
export function nextOrderNumber() {
|
||
|
|
const counter = loadCounter();
|
||
|
|
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||
|
|
counter[today] = (counter[today] || 0) + 1;
|
||
|
|
saveCounter(counter);
|
||
|
|
return `ZT-${today}-${String(counter[today]).padStart(3, '0')}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 生成子编号 */
|
||
|
|
export function nextSubNumber(parentId) {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const o = orders.find(x => x.id === parentId);
|
||
|
|
if (!o) return null;
|
||
|
|
return `${parentId}-${String((o.subs || []).length + 1).padStart(2, '0')}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 创建订单 */
|
||
|
|
export function createOrder({ name, contact, contactType, requirements, budget, expectedDays, source }) {
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
const id = nextOrderNumber();
|
||
|
|
const order = {
|
||
|
|
id, name: name || '匿名客户', contact, contactType: contactType || '微信',
|
||
|
|
requirements: requirements || '', budget: budget || '',
|
||
|
|
expectedDays: expectedDays || 0,
|
||
|
|
expectedDelivery: '',
|
||
|
|
progress: 0, progressNote: '等待接单',
|
||
|
|
source: source || 'web',
|
||
|
|
status: 'submitted', subs: [], comments: [],
|
||
|
|
timeline: [{ at: now, action: 'created', detail: '订单已提交' }],
|
||
|
|
flagged: false, flagReason: '',
|
||
|
|
archived: false,
|
||
|
|
createdAt: now, updatedAt: now,
|
||
|
|
};
|
||
|
|
const orders = loadOrders();
|
||
|
|
orders.push(order);
|
||
|
|
saveOrders(orders);
|
||
|
|
return order;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 获取未归档订单 */
|
||
|
|
export function getActiveOrders() {
|
||
|
|
return loadOrders().filter(o => !o.archived).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 获取已归档订单(历史) */
|
||
|
|
export function getArchivedOrders() {
|
||
|
|
return loadHistory().sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 获取全部(含归档) */
|
||
|
|
export function getAllOrders() {
|
||
|
|
return [...loadOrders(), ...loadHistory()].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 搜索 */
|
||
|
|
export function findOrder(query) {
|
||
|
|
const all = getAllOrders();
|
||
|
|
return all.filter(o =>
|
||
|
|
o.id.toLowerCase().includes(query.toLowerCase()) ||
|
||
|
|
(o.contact || '').includes(query) ||
|
||
|
|
(o.name || '').includes(query)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 按 ID 获取(查活跃+历史) */
|
||
|
|
export function getOrder(id) {
|
||
|
|
return loadOrders().find(o => o.id === id) || loadHistory().find(o => o.id === id) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 更新状态 */
|
||
|
|
export function updateStatus(id, status) {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const order = orders.find(o => o.id === id);
|
||
|
|
if (!order) return null;
|
||
|
|
const valid = ['submitted', 'reviewing', 'accepted', 'in-progress', 'testing', 'completed', 'delivered'];
|
||
|
|
if (!valid.includes(status)) return null;
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
order.status = status;
|
||
|
|
order.updatedAt = now;
|
||
|
|
const labels = { submitted:'已提交', reviewing:'审核中', accepted:'已接单', 'in-progress':'制作中', testing:'测试中', completed:'已完成', delivered:'已交付' };
|
||
|
|
order.timeline.push({ at: now, action: 'status', detail: `状态 → ${labels[status] || status}` });
|
||
|
|
// 接单时算预计交付日
|
||
|
|
if (status === 'accepted' && order.expectedDays > 0) {
|
||
|
|
const d = new Date(); d.setDate(d.getDate() + order.expectedDays);
|
||
|
|
order.expectedDelivery = d.toISOString().slice(0, 10);
|
||
|
|
order.progressNote = `预计 ${order.expectedDays} 天内交付`;
|
||
|
|
}
|
||
|
|
saveOrders(orders);
|
||
|
|
return order;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 更新进度百分比 + 说明 */
|
||
|
|
export function updateProgress(id, progress, note = '') {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const order = orders.find(o => o.id === id);
|
||
|
|
if (!order) return null;
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
order.progress = Math.max(0, Math.min(100, progress));
|
||
|
|
order.progressNote = note || order.progressNote;
|
||
|
|
order.updatedAt = now;
|
||
|
|
order.timeline.push({ at: now, action: 'progress', detail: `进度更新: ${order.progress}% - ${order.progressNote}` });
|
||
|
|
saveOrders(orders);
|
||
|
|
return order;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 设定预期天数(接单时) */
|
||
|
|
export function setExpectedDays(id, days) {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const order = orders.find(o => o.id === id);
|
||
|
|
if (!order) return null;
|
||
|
|
order.expectedDays = days;
|
||
|
|
const d = new Date(); d.setDate(d.getDate() + days);
|
||
|
|
order.expectedDelivery = d.toISOString().slice(0, 10);
|
||
|
|
order.progressNote = `预计 ${days} 天内交付`;
|
||
|
|
order.updatedAt = new Date().toISOString();
|
||
|
|
order.timeline.push({ at: order.updatedAt, action: 'estimate', detail: `设定预期 ${days} 天交付` });
|
||
|
|
saveOrders(orders);
|
||
|
|
return order;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 交付(标记为已交付) */
|
||
|
|
export function deliverOrder(id) {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const order = orders.find(o => o.id === id);
|
||
|
|
if (!order) return null;
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
order.status = 'delivered';
|
||
|
|
order.progress = 100;
|
||
|
|
order.progressNote = '已完成并交付';
|
||
|
|
order.updatedAt = now;
|
||
|
|
order.timeline.push({ at: now, action: 'deliver', detail: '订单已交付给客户' });
|
||
|
|
saveOrders(orders);
|
||
|
|
return order;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 归档订单(移入历史) */
|
||
|
|
export function archiveOrder(id) {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const idx = orders.findIndex(o => o.id === id);
|
||
|
|
if (idx === -1) return null;
|
||
|
|
const order = orders[idx];
|
||
|
|
order.archived = true;
|
||
|
|
order.updatedAt = new Date().toISOString();
|
||
|
|
order.timeline.push({ at: order.updatedAt, action: 'archive', detail: '订单已归档' });
|
||
|
|
const history = loadHistory();
|
||
|
|
history.push(order);
|
||
|
|
saveHistory(history);
|
||
|
|
orders.splice(idx, 1);
|
||
|
|
saveOrders(orders);
|
||
|
|
return order;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 子任务管理 */
|
||
|
|
export function addSubTask(id, { title, description }) {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const order = orders.find(o => o.id === id);
|
||
|
|
if (!order) return null;
|
||
|
|
const subId = nextSubNumber(id);
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
const sub = { subId, title: title || '', description: description || '', status: 'pending', createdAt: now };
|
||
|
|
order.subs = order.subs || [];
|
||
|
|
order.subs.push(sub);
|
||
|
|
order.updatedAt = now;
|
||
|
|
order.timeline.push({ at: now, action: 'subtask', detail: `添加子任务: ${subId} - ${title}` });
|
||
|
|
saveOrders(orders);
|
||
|
|
return sub;
|
||
|
|
}
|
||
|
|
export function updateSubStatus(id, subId, status) {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const order = orders.find(o => o.id === id);
|
||
|
|
if (!order) return null;
|
||
|
|
const sub = order.subs?.find(s => s.subId === subId);
|
||
|
|
if (!sub) return null;
|
||
|
|
sub.status = status;
|
||
|
|
sub.updatedAt = new Date().toISOString();
|
||
|
|
order.updatedAt = sub.updatedAt;
|
||
|
|
order.timeline.push({ at: sub.updatedAt, action: 'subtask', detail: `子任务 ${subId} → ${status}` });
|
||
|
|
saveOrders(orders);
|
||
|
|
return sub;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 评论 */
|
||
|
|
export function addComment(id, { author, content, side }) {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const order = orders.find(o => o.id === id);
|
||
|
|
if (!order) return null;
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
const comment = { id: `c-${Date.now()}`, author: author || '客户', content: content || '', side: side || 'client', createdAt: now };
|
||
|
|
order.comments = order.comments || [];
|
||
|
|
order.comments.push(comment);
|
||
|
|
order.updatedAt = now;
|
||
|
|
order.timeline.push({ at: now, action: 'comment', detail: `${side === 'dev' ? '开发者' : '客户'}留言` });
|
||
|
|
saveOrders(orders);
|
||
|
|
return comment;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 标记可疑 */
|
||
|
|
export function flagOrder(id, flagged, reason = '') {
|
||
|
|
const orders = loadOrders();
|
||
|
|
const order = orders.find(o => o.id === id);
|
||
|
|
if (!order) return null;
|
||
|
|
order.flagged = flagged;
|
||
|
|
order.flagReason = reason;
|
||
|
|
order.updatedAt = new Date().toISOString();
|
||
|
|
order.timeline.push({ at: order.updatedAt, action: 'flag', detail: flagged ? `标记可疑: ${reason}` : '解除可疑标记' });
|
||
|
|
saveOrders(orders);
|
||
|
|
return order;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 统计 */
|
||
|
|
export function getFlaggedOrders() { return loadOrders().filter(o => o.flagged); }
|
||
|
|
export function getRecentOrders(min = 60) {
|
||
|
|
const cutoff = Date.now() - min * 60000;
|
||
|
|
return loadOrders().filter(o => new Date(o.createdAt).getTime() > cutoff);
|
||
|
|
}
|
||
|
|
export function getPendingCount() {
|
||
|
|
return loadOrders().filter(o => !o.archived && ['submitted', 'reviewing'].includes(o.status)).length;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 删除订单 */
|
||
|
|
export function deleteOrder(id) {
|
||
|
|
let orders = loadOrders();
|
||
|
|
const idx = orders.findIndex(o => o.id === id);
|
||
|
|
if (idx !== -1) { orders.splice(idx, 1); saveOrders(orders); return true; }
|
||
|
|
let history = loadHistory();
|
||
|
|
const hidx = history.findIndex(o => o.id === id);
|
||
|
|
if (hidx !== -1) { history.splice(hidx, 1); saveHistory(history); return true; }
|
||
|
|
return false;
|
||
|
|
}
|