huangning
2026-07-18 c8c5a62d62b9ad05cfa0b5e026496b56a735a18f
work/crm_full_server.js
@@ -1,4 +1,5 @@
const http = require("http");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
@@ -33,6 +34,9 @@
  "reports:view",
  "accounts:manage"
]);
const accountWriteLocks = new Map();
const passwordChangePath = "/api/v1/me/password/change";
const passwordChangeBodyLimit = 16 * 1024;
const defaultUsers = [
  { username: "admin", password: "123456", name: "主管", role: "supervisor" },
  { username: "ops1", password: "123456", name: "运营一", role: "ops" },
@@ -130,6 +134,30 @@
function accountFromToken(token, data) {
  if (!token) return null;
  return getUsers(data).find((user) => user.status !== "disabled" && makeToken(user) === token) || null;
}
function accountFromTokenIncludingDisabled(token, data) {
  if (!token) return null;
  return getUsers(data).find((user) => makeToken(user) === token) || null;
}
function bearerTokenFromHeader(req) {
  const auth = String(req.headers.authorization || "");
  return auth.startsWith("Bearer ") && auth.length > 7 ? auth.slice(7) : "";
}
function passwordChangeError(status, code, message) {
  const err = new Error(message);
  err.status = status;
  err.passwordChangeCode = code;
  return err;
}
function dataVersionConflictError() {
  const err = new Error("数据已被其他请求更新");
  err.status = 409;
  err.code = "DATA_VERSION_CONFLICT";
  return err;
}
function requireAccount(req, url, body = {}, data = load()) {
@@ -414,11 +442,55 @@
  return data;
}
function save(data) {
  data._version = (Number(data._version) || 0) + 1;
  const tmpFile = `${dataFile}.${process.pid}.tmp`;
  fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2), "utf8");
  fs.renameSync(tmpFile, dataFile);
function dataVersion(data) {
  const value = Number(data && data._version);
  return Number.isSafeInteger(value) && value >= 0 ? value : 0;
}
function save(data, expectedVersion = dataVersion(data)) {
  ensureData();
  const stored = JSON.parse(fs.readFileSync(dataFile, "utf8"));
  if (dataVersion(stored) !== expectedVersion) throw dataVersionConflictError();
  const nextData = { ...data, _version: expectedVersion + 1 };
  const tmpFile = `${dataFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
  try {
    fs.writeFileSync(tmpFile, JSON.stringify(nextData, null, 2), "utf8");
    fs.renameSync(tmpFile, dataFile);
    data._version = nextData._version;
  } finally {
    if (fs.existsSync(tmpFile)) {
      try { fs.unlinkSync(tmpFile); } catch (_) { /* best-effort cleanup */ }
    }
  }
}
async function withAccountWriteLock(accountKey, task) {
  const key = String(accountKey);
  const previous = accountWriteLocks.get(key) || Promise.resolve();
  let release;
  const current = new Promise((resolve) => { release = resolve; });
  accountWriteLocks.set(key, current);
  await previous;
  try {
    return await task();
  } finally {
    release();
    if (accountWriteLocks.get(key) === current) accountWriteLocks.delete(key);
  }
}
function mutateAccountWithCas(accountKey, expectedVersion, mutator, store = { load, save }) {
  return withAccountWriteLock(accountKey, () => {
    const data = store.load();
    if (dataVersion(data) !== expectedVersion) throw dataVersionConflictError();
    const result = mutator(data);
    if (result && typeof result.then === "function") {
      throw new Error("账号写入回调必须同步完成");
    }
    store.save(data, expectedVersion);
    return result;
  });
}
function requireSupervisor(account) {
@@ -427,6 +499,12 @@
    err.status = 403;
    throw err;
  }
}
function httpError(status, message) {
  const err = new Error(message);
  err.status = status;
  return err;
}
function action(data, customer, user, type, title, content) {
@@ -450,6 +528,137 @@
      try { resolve(raw ? JSON.parse(raw) : {}); } catch (err) { reject(err); }
    });
  });
}
function parsePasswordChangeBody(req) {
  return new Promise((resolve, reject) => {
    let raw = "";
    let size = 0;
    let tooLarge = false;
    req.on("data", (chunk) => {
      size += chunk.length;
      if (size > passwordChangeBodyLimit) {
        tooLarge = true;
        raw = "";
      } else if (!tooLarge) {
        raw += chunk;
      }
    });
    req.on("end", () => {
      if (tooLarge || !raw) {
        reject(passwordChangeError(400, "INVALID_REQUEST", "请求无效"));
        return;
      }
      try {
        resolve(JSON.parse(raw));
      } catch (_) {
        reject(passwordChangeError(400, "INVALID_REQUEST", "请求无效"));
      }
    });
    req.on("aborted", () => reject(passwordChangeError(400, "INVALID_REQUEST", "请求无效")));
    req.on("error", () => reject(passwordChangeError(400, "INVALID_REQUEST", "请求无效")));
  });
}
function validatePasswordChangeEnvelope(req, url) {
  const contentType = String(req.headers["content-type"] || "").split(";", 1)[0].trim().toLowerCase();
  if (contentType !== "application/json" || !url.searchParams.keys().next().done) {
    throw passwordChangeError(400, "INVALID_REQUEST", "请求无效");
  }
}
function validatePasswordChangePayload(body) {
  if (!body || Array.isArray(body) || typeof body !== "object") {
    throw passwordChangeError(400, "INVALID_REQUEST", "请求无效");
  }
  const keys = Object.keys(body);
  const allowed = new Set(["currentPassword", "newPassword"]);
  if (keys.length !== 2 || keys.some((key) => !allowed.has(key))
    || typeof body.currentPassword !== "string" || typeof body.newPassword !== "string") {
    throw passwordChangeError(400, "INVALID_REQUEST", "请求无效");
  }
}
function passwordChangeRequestId() {
  return `pwd-${crypto.randomUUID()}`;
}
function writePasswordChangeError(res, err, requestId) {
  const body = JSON.stringify({
    error: {
      code: err.passwordChangeCode,
      message: err.message,
      requestId
    }
  });
  res.writeHead(err.status, {
    "Content-Type": "application/json; charset=utf-8",
    "Cache-Control": "no-store"
  });
  res.end(body);
}
function normalizedPasswordChangeError(err) {
  if (err && err.passwordChangeCode) return err;
  if (err && err.code === "DATA_VERSION_CONFLICT") {
    return passwordChangeError(409, "PASSWORD_UPDATE_CONFLICT", "密码状态已变化,请重新登录");
  }
  if (err instanceof SyntaxError || (err && typeof err.code === "string" && /^E[A-Z]+$/.test(err.code))) {
    return passwordChangeError(503, "PASSWORD_CHANGE_UNAVAILABLE", "密码修改暂不可用");
  }
  return passwordChangeError(500, "INTERNAL_ERROR", "服务暂时无法处理请求");
}
async function handlePasswordChange(req, res, url, store = { load, save }) {
  const requestId = passwordChangeRequestId();
  try {
    validatePasswordChangeEnvelope(req, url);
    const body = await parsePasswordChangeBody(req);
    validatePasswordChangePayload(body);
    let initialData;
    try {
      initialData = store.load();
    } catch (err) {
      throw normalizedPasswordChangeError(err);
    }
    const token = bearerTokenFromHeader(req);
    const authenticated = accountFromTokenIncludingDisabled(token, initialData);
    if (!authenticated) {
      throw passwordChangeError(401, "AUTHENTICATION_REQUIRED", "请重新登录");
    }
    const expectedVersion = dataVersion(initialData);
    await mutateAccountWithCas(authenticated.username, expectedVersion, (latestData) => {
      const latest = getUsers(latestData).find((row) => row.username === authenticated.username);
      if (!latest) {
        throw passwordChangeError(409, "PASSWORD_UPDATE_CONFLICT", "密码状态已变化,请重新登录");
      }
      if (latest.status === "disabled") {
        throw passwordChangeError(403, "ACCOUNT_DISABLED", "账号不可用");
      }
      if (makeToken(latest) !== token) {
        throw passwordChangeError(409, "PASSWORD_UPDATE_CONFLICT", "密码状态已变化,请重新登录");
      }
      if (latest.password !== body.currentPassword) {
        throw passwordChangeError(422, "CURRENT_PASSWORD_INCORRECT", "当前密码不正确");
      }
      const codePointLength = Array.from(body.newPassword).length;
      if (codePointLength < 8 || codePointLength > 64) {
        throw passwordChangeError(422, "PASSWORD_LENGTH_INVALID", "新密码长度必须为8至64个字符");
      }
      if (body.newPassword === body.currentPassword) {
        throw passwordChangeError(422, "NEW_PASSWORD_SAME_AS_CURRENT", "新密码不能与当前密码相同");
      }
      latest.password = body.newPassword;
      latest.updatedAt = nowText();
    }, store);
    res.writeHead(204, { "Cache-Control": "no-store" });
    res.end();
  } catch (err) {
    writePasswordChangeError(res, normalizedPasswordChangeError(err), requestId);
  }
}
function remindersFor(data, account) {
@@ -480,9 +689,13 @@
  return rows;
}
async function handleApi(req, res) {
async function handleApi(req, res, options = {}) {
  const url = new URL(req.url, "http://127.0.0.1");
  if (req.method === "POST" && url.pathname === passwordChangePath) {
    await handlePasswordChange(req, res, url, options.passwordStore || { load, save });
    return;
  }
  const data = load();
  const url = new URL(req.url, `http://${req.headers.host}`);
  if (req.method === "POST" && url.pathname === "/api/login") {
    const body = await parseBody(req);
@@ -559,44 +772,45 @@
      json(res, 400, { error: "请填写账号、姓名和岗位" });
      return;
    }
    const accountRows = getUsers(data);
    let row = accountRows.find((item) => item.username === username);
    if (!row && accountRows.some((item) => item.phone === phone)) {
      json(res, 409, { error: "这个手机号已经存在账号" });
      return;
    }
    if (!row && !password) {
      json(res, 400, { error: "新增账号必须设置初始密码" });
      return;
    }
    if (row && row.username === account.username && status === "disabled") {
      json(res, 400, { error: "不能停用当前登录的主管账号" });
      return;
    }
    if (row) {
      row.name = name;
      row.phone = phone;
      row.role = roleValue;
      row.status = status;
      row.permissions = permissions;
      if (password) row.password = password;
      row.updatedAt = nowText();
    } else {
      row = {
        username,
        phone,
        password,
        name,
        role: roleValue,
        status,
        permissions,
        createdAt: nowText(),
        updatedAt: ""
      };
      accountRows.push(row);
    }
    save(data);
    json(res, 200, { account: publicUser(row), accounts: accountRows.map(publicUser) });
    const result = await mutateAccountWithCas(username, dataVersion(data), (latestData) => {
      const latestActor = getUsers(latestData).find((item) => item.username === account.username && item.status !== "disabled");
      if (!latestActor) throw httpError(401, "请先登录账号");
      requireSupervisor(latestActor);
      const accountRows = getUsers(latestData);
      let row = accountRows.find((item) => item.username === username);
      if (!row && accountRows.some((item) => item.phone === phone)) {
        throw httpError(409, "这个手机号已经存在账号");
      }
      if (!row && !password) throw httpError(400, "新增账号必须设置初始密码");
      if (row && row.username === latestActor.username && status === "disabled") {
        throw httpError(400, "不能停用当前登录的主管账号");
      }
      if (row) {
        row.name = name;
        row.phone = phone;
        row.role = roleValue;
        row.status = status;
        row.permissions = permissions;
        if (password) row.password = password;
        row.updatedAt = nowText();
      } else {
        row = {
          username,
          phone,
          password,
          name,
          role: roleValue,
          status,
          permissions,
          createdAt: nowText(),
          updatedAt: ""
        };
        accountRows.push(row);
      }
      return { account: publicUser(row), accounts: accountRows.map(publicUser) };
    });
    json(res, 200, result);
    return;
  }
@@ -608,15 +822,19 @@
      json(res, 400, { error: "不能修改当前登录账号状态" });
      return;
    }
    const row = getUsers(data).find((item) => item.username === username);
    if (!row) {
      json(res, 404, { error: "账号不存在" });
      return;
    }
    row.status = status;
    row.updatedAt = nowText();
    save(data);
    json(res, 200, { account: publicUser(row), accounts: getUsers(data).map(publicUser) });
    const result = await mutateAccountWithCas(username, dataVersion(data), (latestData) => {
      const latestActor = getUsers(latestData).find((item) => item.username === account.username && item.status !== "disabled");
      if (!latestActor) throw httpError(401, "请先登录账号");
      requireSupervisor(latestActor);
      const accountRows = getUsers(latestData);
      const row = accountRows.find((item) => item.username === username);
      if (!row) throw httpError(404, "账号不存在");
      row.status = status;
      row.updatedAt = nowText();
      return { account: publicUser(row), accounts: accountRows.map(publicUser) };
    });
    json(res, 200, result);
    return;
  }
@@ -1049,15 +1267,28 @@
  });
}
const server = http.createServer((req, res) => {
  if (req.url.startsWith("/api/")) {
    handleApi(req, res).catch((err) => json(res, err.status || 500, { error: err.message }));
  } else {
    serveFile(req, res);
  }
});
function createServer(options = {}) {
  return http.createServer((req, res) => {
    if (req.url.startsWith("/api/")) {
      handleApi(req, res, options).catch((err) => json(res, err.status || 500, { error: err.message }));
    } else {
      serveFile(req, res);
    }
  });
}
ensureData();
server.listen(port, "0.0.0.0", () => {
  console.log(`Full CRM demo is running at http://127.0.0.1:${port}/`);
});
if (require.main === module) {
  ensureData();
  const server = createServer();
  server.listen(port, "0.0.0.0", () => {
    console.log(`Full CRM demo is running at http://127.0.0.1:${port}/`);
  });
}
module.exports = {
  createServer,
  dataVersion,
  makeToken,
  mutateAccountWithCas,
  validatePasswordChangePayload
};