1 line
58 KiB
JSON
1 line
58 KiB
JSON
{"repo": "3330195271-oss/arocx", "files": {"server/src/ocr-service.ts": {"0ed5b7a2934aae20a8420d02784202ea64bb7547": {"t": 1785768942.0443814, "n": 3779, "h": "58eaac9ae9eddb4e", "c": "export interface OcrResult {\n name: string\n phone: string\n address: string\n deviceId: string\n shipmentDate: string\n rentalStart: string\n rentalEnd: string\n platform: string\n csRep: string\n remarks: string\n}\n\nconst SYSTEM_PROMPT = `你是一个订单信息提取器。请从截图中提取以下字段,以 JSON 格式返回。\n找不到的字段返回空字符串 \"\"。\n\n{\n \"name\": \"收货人姓名\",\n \"phone\": \"手机号(11位)\",\n \"address\": \"完整收货地址,逐字提取不遗漏\",\n \"deviceId\": \"租赁设备型号\",\n \"shipmentDate\": \"发货日期\",\n \"rentalStart\": \"起租日期\",\n \"rentalEnd\": \"到期日期\",\n \"platform\": \"订单平台(如淘宝、诚赁)\",\n \"csRep\": \"客服名字\",\n \"remarks\": \"备注\"\n}\n\n只返回JSON,不要其他文字。`\n\nfunction clean(value: string): string {\n if (!value || value === '无' || value === '无信息' || value === '未提供') return ''\n return value.trim()\n}\n\nfunction normalizeDate(value: string): string {\n if (!value) return ''\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return value\n\n let match = value.match(/(\\d{4})年(\\d{1,2})月(\\d{1,2})日?/)\n if (match) return `${match[1]}-${match[2].padStart(2, '0')}-${match[3].padStart(2, '0')}`\n\n match = value.match(/^(\\d{1,2})\\.(\\d{1,2})$/)\n if (match) {\n const month = parseInt(match[1], 10)\n const day = parseInt(match[2], 10)\n if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {\n return `${new Date().getFullYear()}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`\n }\n }\n\n return value\n}\n\nfunction resolveOcrRuntime(apiKey: string): { apiBase: string; model: string } {\n const provider = (process.env.OCR_PROVIDER || '').toLowerCase()\n\n if (provider === 'dashscope' || apiKey.startsWith('sk-ws-')) {\n return {\n apiBase: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n model: process.env.OCR_MODEL || 'qwen-vl-max'\n }\n }\n\n if (apiKey.startsWith('fk')) {\n return {\n apiBase: 'https://oa.api2d.net',\n model: process.env.OCR_MODEL || 'gpt-4o-mini'\n }\n }\n\n return {\n apiBase: 'https://api.openai.com',\n model: process.env.OCR_MODEL || 'gpt-4o-mini'\n }\n}\n\nexport async function extractOrderFromImageWithKey(apiKey: string, base64Image: string): Promise<OcrResult> {\n const { apiBase, model } = resolveOcrRuntime(apiKey.trim())\n const imageUrl = base64Image.startsWith('data:image/')\n ? base64Image\n : `data:image/png;base64,${base64Image}`\n\n const response = await fetch(`${apiBase}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiKey.trim()}`\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: SYSTEM_PROMPT },\n {\n role: 'user',\n content: [\n {\n type: 'image_url',\n image_url: {\n url: imageUrl,\n detail: 'high'\n }\n }\n ]\n }\n ],\n max_tokens: 500,\n temperature: 0\n })\n })\n\n if (!response.ok) {\n const errText = await response.text()\n throw new Error(`识别失败 (${response.status}): ${errText.slice(0, 300)}`)\n }\n\n const data: any = await response.json()\n const content = data.choices?.[0]?.message?.content || ''\n const jsonStr = content.trim()\n .replace(/^```(?:json)?\\s*/i, '')\n .replace(/\\s*```$/, '')\n\n try {\n const result: OcrResult = JSON.parse(jsonStr)\n return {\n name: clean(result.name),\n phone: clean(result.phone),\n address: clean(result.address),\n deviceId: clean(result.deviceId),\n shipmentDate: normalizeDate(clean(result.shipmentDate)),\n rentalStart: normalizeDate(clean(result.rentalStart)),\n rentalEnd: normalizeDate(clean(result.rentalEnd)),\n platform: clean(result.platform),\n csRep: clean(result.csRep),\n remarks: clean(result.remarks)\n }\n } catch {\n throw new Error(`解析失败,原始输出:${content.slice(0, 200)}`)\n }\n}\n"}}, "src/main/ocr-service.ts": {"0ed5b7a2934aae20a8420d02784202ea64bb7547": {"t": 1785768942.2121525, "n": 4857, "h": "3c4600214ef7b828", "c": "/**\n * OCR Service — 通义千问 VL 视觉识别(DashScope API)\n *\n * 支持:qwen-vl-max(默认,中文OCR最强)/ gpt-4o-mini(API2D备用)\n * API Key 优先级:OCR_API_KEY > DASHSCOPE_API_KEY > OPENAI_API_KEY > data/api-key.txt\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'\nimport { join } from 'path'\nimport { getAppDataDir } from './app-data-dir'\n\nexport interface OcrResult {\n name: string\n phone: string\n address: string\n deviceId: string\n shipmentDate: string\n rentalStart: string\n rentalEnd: string\n platform: string\n csRep: string\n remarks: string\n}\n\nconst SYSTEM_PROMPT = `你是一个订单信息提取器。请从截图中提取以下字段,以 JSON 格式返回。\n找不到的字段返回空字符串 \"\"。\n\n{\n \"name\": \"收货人姓名\",\n \"phone\": \"手机号(11位)\",\n \"address\": \"完整收货地址,逐字提取不遗漏\",\n \"deviceId\": \"租赁设备型号\",\n \"shipmentDate\": \"发货日期\",\n \"rentalStart\": \"起租日期\",\n \"rentalEnd\": \"到期日期\",\n \"platform\": \"订单平台(如淘宝、诚赁)\",\n \"csRep\": \"客服名字\",\n \"remarks\": \"备注\"\n}\n\n只返回JSON,不要其他文字。`\n\nfunction getDataDir(): string {\n const dir = getAppDataDir()\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true })\n return dir\n}\n\nfunction getApiKeyPath(): string {\n return join(getDataDir(), 'api-key.txt')\n}\n\nexport function getApiKey(): string {\n const envKey = process.env.OCR_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.OPENAI_API_KEY\n if (envKey?.trim()) return envKey.trim()\n\n const keyPath = getApiKeyPath()\n if (!existsSync(keyPath)) return ''\n return readFileSync(keyPath, 'utf-8').trim()\n}\n\nexport function getMaskedApiKey(): string {\n const key = getApiKey()\n if (!key) return ''\n if (key.length <= 12) return '已配置'\n return `${key.slice(0, 4)}...${key.slice(-4)}`\n}\n\nexport function saveApiKey(key: string): void {\n writeFileSync(getApiKeyPath(), key.trim(), { encoding: 'utf-8', mode: 0o600 })\n}\n\nexport async function extractOrderFromImage(base64Image: string): Promise<OcrResult> {\n const apiKey = getApiKey()\n if (!apiKey) throw new Error('未配置 OCR API Key,请设置 OCR_API_KEY / OPENAI_API_KEY 或 data/api-key.txt')\n\n // Detect API provider\n let apiBase: string\n let model: string\n const provider = (process.env.OCR_PROVIDER || '').toLowerCase()\n\n if (provider === 'dashscope' || apiKey.startsWith('sk-ws-')) {\n // DashScope (通义千问)\n apiBase = 'https://dashscope.aliyuncs.com/compatible-mode/v1'\n model = process.env.OCR_MODEL || 'qwen-vl-max'\n } else if (apiKey.startsWith('fk')) {\n // API2D\n apiBase = 'https://oa.api2d.net'\n model = process.env.OCR_MODEL || 'gpt-4o-mini'\n } else {\n // OpenAI official\n apiBase = 'https://api.openai.com'\n model = process.env.OCR_MODEL || 'gpt-4o-mini'\n }\n\n const imageUrl = base64Image.startsWith('data:image/')\n ? base64Image\n : `data:image/png;base64,${base64Image}`\n\n const response = await fetch(`${apiBase}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: SYSTEM_PROMPT },\n {\n role: 'user',\n content: [\n {\n type: 'image_url',\n image_url: {\n url: imageUrl,\n detail: 'high'\n }\n }\n ]\n }\n ],\n max_tokens: 500,\n temperature: 0\n })\n })\n\n if (!response.ok) {\n const err = await response.text()\n throw new Error(`识别失败 (${response.status}): ${err.slice(0, 300)}`)\n }\n\n const data = await response.json()\n const content = data.choices?.[0]?.message?.content || ''\n\n let jsonStr = content.trim()\n .replace(/^```(?:json)?\\s*/i, '')\n .replace(/\\s*```$/, '')\n\n try {\n const result: OcrResult = JSON.parse(jsonStr)\n return {\n name: clean(result.name),\n phone: clean(result.phone),\n address: clean(result.address),\n deviceId: clean(result.deviceId),\n shipmentDate: normalizeDate(clean(result.shipmentDate)),\n rentalStart: normalizeDate(clean(result.rentalStart)),\n rentalEnd: normalizeDate(clean(result.rentalEnd)),\n platform: clean(result.platform),\n csRep: clean(result.csRep),\n remarks: clean(result.remarks)\n }\n } catch {\n throw new Error(`解析失败,原始输出:${content.slice(0, 200)}`)\n }\n}\n\nfunction clean(s: string): string {\n if (!s || s === '无' || s === '无信息' || s === '未提供') return ''\n return s.trim()\n}\n\nfunction normalizeDate(s: string): string {\n if (!s) return ''\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return s\n\n let m = s.match(/(\\d{4})年(\\d{1,2})月(\\d{1,2})日?/)\n if (m) return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`\n\n m = s.match(/^(\\d{1,2})\\.(\\d{1,2})$/)\n if (m) {\n const mo = parseInt(m[1]), d = parseInt(m[2])\n if (mo >= 1 && mo <= 12 && d >= 1 && d <= 31) {\n return `${new Date().getFullYear()}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`\n }\n }\n return s\n}\n"}}, "DEVELOPMENT_LOG.md": {"0ed5b7a2934aae20a8420d02784202ea64bb7547": {"t": 1785768942.992888, "n": 27605, "h": "a9e4a3ce3a0ea2ab", "c": "# 开发日志 — AROCX / ARO 仓序\n\n## 2026-06-26:1.0.9 发布记录\n\n### 本次发布目标\n\n这一版主要处理两类高频操作体验:\n\n1. 企业协作下的数据刷新不要太频繁,但也要保留随时查看最新数据的能力。\n2. 发货时如果现场拿到的序列号还没提前入库,应该可以直接补设备并发货,而不是先退出去再录一遍库存。\n\n### 关键功能变更\n\n#### 1. 企业工作区自动刷新改为每 30 分钟\n\n- 云端同步服务的企业工作区轮询间隔由 30 秒调整为 30 分钟。\n- 这样能减少多人协作时不必要的频繁拉取,也降低本地与云端持续同步带来的干扰。\n- 同时保留原有的企业主工作区模型,企业成员看到的依然是企业统一数据。\n\n#### 2. 顶部新增“手动同步”按钮\n\n- 在主界面顶部工具栏加入了 `手动同步` 按钮。\n- 企业成员如果知道同事刚刚改过订单、库存或发货记录,可以立即点击手动同步拉取最新数据。\n- 手动同步走的是“只拉云端最新数据”的流程,不会先把当前机器的旧快照反推回服务器。\n\n#### 3. 企业页面同步提示文案统一\n\n- 订单管理、设备库存、发货信息三个页面的企业提示文案已统一。\n- 现在会明确告诉用户:\n - 默认每 30 分钟自动刷新一次\n - 也可以点击顶部手动同步立即查看最新数据\n\n#### 4. 发货弹窗支持“入库并发货”\n\n- 在订单管理点击“发货”后,如果输入的序列号当前不在库存中,界面会直接出现 `入库并发货` 操作。\n- 这样在实际发货场景里,不需要先退出订单页去库存页补设备,再返回来完成发货。\n- 若输入的序列号其实已经存在,但设备当前不是空闲状态,也会明确提示不可直接发货,避免误操作。\n\n#### 5. 本地端与企业端发货链路同时补齐\n\n- Electron 本地数据层新增“创建设备并发货”的处理函数。\n- 企业服务端新增接口:\n - `POST /api/enterprise/orders/:orderId/dispatch-with-new-device`\n- 这样无论是个人工作区还是企业工作区,都能使用同样的“入库并发货”能力。\n- 企业端的服务端校验也一并补齐,确保不同电脑在同一订单上的处理结果一致。\n\n### 官网与发布信息\n\n- 官网静态兜底版本已更新到 1.0.9。\n- 官网下载链接与 GitHub Releases 下载地址已经同步切到 1.0.9。\n- 首页更新公告也已改为本次版本内容,防止官网展示仍停留在 1.0.8。\n\n### 验证结果\n\n本次改动完成后,已做以下验证:\n\n- `npm run build`\n- `npm run typecheck`\n- `npx tsc -p server/tsconfig.json --noEmit`\n\n### 备注\n\n- 这一版属于协作体验与发货流程收口版,没有改动付费权益和账号体系。\n- Windows 自动更新会在发布后继续沿用 GitHub Release 资源。\n\n## 2026-06-26:1.0.8 发布记录\n\n### 本次发布目标\n\n这一版主要把前面几轮已经定下来的协作逻辑真正收口,重点是三件事:\n\n1. 企业协作必须以企业主的数据空间为准,不能再把成员各自的本地订单拼在一起。\n2. 好友代发要留下完整记录,后续查发货、查责任、查单号都不能断线。\n3. AI 截图录单要改成统一使用服务器端密钥,不能要求每台客户电脑自己配置。\n\n### 关键功能变更\n\n#### 1. 企业协作改为“企业主工作区”\n\n- 服务端新增企业上下文解析逻辑,统一识别企业 ID、企业主账号、成员角色和企业主订阅状态。\n- 企业成员进入订单管理、设备库存、发货信息后,看到的都是企业主账号名下的正式数据。\n- 成员加入企业后会立即执行一次拉取同步,本地工作区会切换到企业主的订单和设备快照。\n- 成员退出企业后也会立刻重新拉取,把界面切回自己的个人工作区。\n- 同步服务新增企业工作区分流:普通账号继续走个人 `/api/sync/*`,企业成员自动切到 `/api/enterprise/sync/*`。\n\n#### 2. 企业工作区的云端同步改成“快照替换”\n\n- 服务端新增企业订单、企业设备的同步接口:\n - `GET /api/enterprise/sync/orders`\n - `POST /api/enterprise/sync/orders`\n - `GET /api/enterprise/sync/devices`\n - `POST /api/enterprise/sync/devices`\n- 推送时不再把成员自己的数据插到企业里,而是按企业主工作区做整份快照更新。\n- 若本次快照里已经不存在某条订单或设备,服务端会同步删除旧记录,避免脏数据残留。\n- 这样可以保证“一个人修改,其他人 30 秒内自动刷新看到”的逻辑真正成立。\n\n#### 3. 好友代发链路补齐\n\n- `orders` 表新增:\n - `friend_dispatch_helper_user_id`\n - `friend_dispatch_helper_email`\n- 好友帮忙发货时,服务端会把代发人的账号信息写回原订单。\n- 发货信息页新增两种标记:\n - 代发人自己查看时:显示“帮好友代发”\n - 原订单所有者查看时:显示“好友帮忙发”\n- 代发记录会进入发货信息,不再是“发了但看不见”的隐形状态。\n\n#### 4. AI 截图录单改为服务器统一密钥\n\n- 新增服务端 OCR 模块:\n - [server/src/ocr-service.ts](/Users/ssdbh/Documents/codex/RJKF/RJKF/server/src/ocr-service.ts)\n - [server/src/routes/ocr.ts](/Users/ssdbh/Documents/codex/RJKF/RJKF/server/src/routes/ocr.ts)\n- 新增接口:`POST /api/ocr/extract`\n- 客户端截图识别不再调用本地 `data/api-key.txt` 或本地环境变量,而是统一请求云端 OCR 接口。\n- 前端删除了本地 API Key 的读写入口,避免客户自行修改识别密钥。\n- 如果服务器未配置 OCR key,前端会明确提示“服务器端还没有配置 AI 识别密钥,请联系管理员处理”。\n\n#### 5. 技术支持体验改版\n\n- 新增 [src/renderer/components/SupportEmailModal.tsx](/Users/ssdbh/Documents/codex/RJKF/RJKF/src/renderer/components/SupportEmailModal.tsx)\n- 首页与设置页的技术支持入口不再直接强制打开邮箱软件。\n- 现在会先弹出邮箱说明框,支持两种动作:\n - 复制邮箱地址\n - 一键调用邮件应用发信\n- 这样在客户电脑没配置默认邮件客户端时,也不会显得像“功能失效”。\n\n#### 6. 官网与下载页更新\n\n- 官网首页的功能标签已经支持点击跳转到对应模块说明。\n- 新增“回到顶部”按钮,长页浏览更顺手。\n- 官网新增 GitHub Releases 入口,用户除了官网下载,也可以直接去 GitHub 获取安装包。\n- 官网静态兜底版本信息已更新到 1.0.8,防止接口暂时异常时页面展示旧版本。\n\n#### 7. 正式版默认接口地址切换到域名\n\n- 客户端默认 API 基地址从裸 IP 改为 `https://arocx.fun`\n- 设置页里的服务器地址占位文案也同步更新。\n- 这样正式版默认会优先走域名入口,减少用户接触裸 IP 和端口号。\n\n### 数据与运维处理\n\n#### 1. 清理测试订单与设备数据\n\n- 已清空服务器中的测试订单、测试设备和订单协作记录。\n- 用户账号、订阅状态、激活信息均已保留。\n- 清理前做了备份,备份文件位置:\n - `/root/server-backups/manual-clears/pre-clear-orders-devices-2026-06-26T04-57-31-732Z.json`\n\n#### 2. 当前正式环境关键位置\n\n- 正式域名:`https://arocx.fun`\n- GitHub Releases:`https://github.com/3330195271-oss/arocx/releases`\n- 云端服务目录:`/root/server`\n- 安装包上传目录:`/root/server/uploads/app`\n- 官网静态文件目录(仓库内):`server/public/`\n- 云端 API 服务:Docker Compose 中的 `api` 服务\n- 数据库服务:Docker Compose 中的 `db` 服务\n\n#### 3. 敏感信息记录策略\n\n- 服务器密码、GitHub Token、邮件授权信息这类敏感内容**不写入共享开发日志,也不进入 Git 仓库**。\n- 共享日志只记录结构、路径、流程、发布位置与排障信息。\n- 这样后续即使仓库推送到 GitHub,也不会把高风险凭证一起带出去。\n\n### 验证结果\n\n本次改动完成后,已做以下验证:\n\n- `npm run typecheck`\n- `npm run build`\n- `npx tsc -p server/tsconfig.json --noEmit`\n\n在发布前还额外核对了两项线上状态:\n\n- 服务器中的测试订单和设备已经清空,避免新版本演示时再混入旧数据。\n- 服务器 OCR 环境变量此前为空,因此本次发布会一并补齐服务器端 OCR 配置,保证截图识别能直接可用。\n\n### 备注\n\n- 下面的历史日志保留了早期“租赁客户转寄助手 / 仓库管理助手”阶段的开发记录,便于回溯旧需求来源。\n- 从 1.0.8 开始,建议以后统一使用 `arocx` / `ARO 仓序` 这套命名做版本、官网、安装包和说明文档。\n\n## 项目概述\n\nWindows 桌面应用,用于读取 WPS 共享表格中的客户租赁数据,自动筛选今日到期客户,并根据地址和日期匹配推荐转寄目标。\n\n## 技术栈\n\n- Electron + React + TypeScript\n- Vite (electron-vite)\n- xlsx 库读取 Excel\n- Apple 风格 UI\n\n## 开发时间线\n\n### 2026-05-15:项目初始化\n\n- 使用 electron-vite 创建 Electron + React + TypeScript 项目\n- 搭建 Apple 风格 UI(毛玻璃侧边栏、圆角卡片、自定义标题栏)\n- 实现地址解析器(省/市/区/街道四级提取)\n- 实现转寄匹配逻辑\n\n### 2026-05-15:API 方案尝试(失败)\n\n**问题1:WPS 开放平台注册混淆**\n- 用户先在 solution.wps.cn 注册,获取 SX 开头的 APPID\n- 后来才在正确的 open.wps.cn 注册,获取 AK 开头的 APPID\n\n**问题2:权限申请流程复杂**\n- 创建应用后需申请 drive(云文档)权限\n- 企业自建应用审批流程:开发者申请 → 企业管理员在 work.wps.cn 审批 → 创建版本 → 发布\n- 关键权限 `interface_company_doc` 不在个人开发者权限列表中,无法开通\n\n**问题3:API 端点不匹配**\n- Token 端点:`https://openapi.wps.cn/oauth2/token`(非 `/oauth/2/token`)\n- 分享链接转 file_token:`/v7/links/{shareCode}/meta`\n- 返回 403 `ErrPrivileges: interface_company_doc`,始终无法通过\n\n**最终放弃 API 方案**,转用本地文件方案。\n\n### 2026-05-15 晚:本地文件方案(成功)\n\n**方案**:用户手动下载共享表格为 .xlsx → 软件读取本地文件\n\n**遇到的问题和解决**:\n\n1. **xlsx 库打包问题**\n - 内置了 xlsx 库读取 Excel,在 Vite SSR 打包时被 Bundle 而非 Externalize\n - 包体积从 8KB 膨胀到 686KB,但不影响功能\n - 改用 `readFileSync` + `XLSX.read(buf)` 方式读取,避免 Windows 路径问题\n\n2. **表头字段匹配失败(0 个客户)**\n - 预设关键词:`姓名、电话、租赁开始、租赁结束`\n - 实际表头:`平台、客户、地址、型号、发货日、起租日、最后一天租期、备注、租金`\n - 修复:新增关键词映射(`客户→name`、`起租日→rentalStart`、`最后一天租期→rentalEnd`、`型号→deviceId`)\n\n3. **姓名和电话嵌入地址字段**\n - 数据格式:`周奇13220301743 重庆渝北区...`\n - 修复:正则提取 `{2-4中文姓名}{11位手机号}` 模式,自动填充 name 和 phone\n\n4. **日期格式解析错误**\n - 原始数据:`6.13`(6月13日)、`5.27`(5月27日)\n - 被错误当作 Excel 序列号转为 `1900-01-05`\n - 修复:`M.D` 格式优先识别(< 100 的小数 = 月.日),大数才按 Excel 序列号处理\n\n5. **地址解析不准确**\n - 原始格式:`上海-上海市-嘉定区-菊园新区秋竹路...`(用 `-` 分隔)\n - 修复:新增 `parseDashFormat` 函数处理破折号分隔的地址\n - 添加 `新区` 到行政区后缀列表\n\n## 匹配规则\n\n### 到期判断\n- 租赁结束日 = 今天(YYYY-MM-DD)\n\n### 转寄匹配(三级)\n| 级别 | 条件 | 颜色 | 图标 |\n|------|------|------|------|\n| 同市 | 同一城市 | 绿色 | ★ |\n| 同省 | 同省不同市 | 蓝色 | ● |\n| 邻省 | 相邻省份 | 橙色 | ○ ⚠注意时效 |\n\n### 必须条件\n- 同一型号(deviceId)\n- 候选客户**发货日 = 今天**\n- 排序:同市 → 同省 → 邻省,同级按日期衔接度\n\n### 邻省表\n内置全国 34 个省级行政区的邻接关系表。\n\n## 数据文件\n\n- 格式:.xlsx(Excel)\n- 路径:开发模式 `data/`,生产模式 `文档\\仓库管理助手\\`\n- 自动读取目录下最新修改的 `.xlsx` 文件,不限制文件名\n- 操作流程:点「📂 打开文件夹」→ 放入 Excel → 点「读取数据」\n\n## 已知问题\n\n1. 邻省匹配未考虑实际物流距离(仅行政邻接)\n2. 打包后约 78MB(含 Electron 运行时 + express/multer/xlsx)\n3. 多人同时操作时无并发锁(JSON 文件读写可能冲突)\n\n---\n\n## 2026-06-05:设备管理系统升级 + Web 多人协作\n\n### 第一阶段:显示发货日和起租日\n\n**问题**:转寄推荐卡片不显示候选客户的发货日和起租日期。\n\n**修复**:\n- Customer 类型新增 `shipmentDate`(发货日)字段,与 `rentalStart`(起租日)分离\n- wps-service.ts 中「发货日」单独映射到 `shipmentDate`,不再与「起租日」合并\n- ForwardingList.tsx 和 Sidebar.tsx 卡片增加日期行显示\n\n### 第二阶段:转型为设备管理系统\n\n将软件从单纯租赁转寄推荐升级为完整的设备生命周期管理系统。\n\n**新增四个核心视图**:\n| Tab | 功能 |\n|-----|------|\n| 📊 仪表盘 | 库存概览 + 今日发货/归还/剩余统计 |\n| 📦 设备库存 | 手动入库、Excel 批量导入、状态筛选、删除 |\n| 📋 订单管理 | 今日订单、发货(选序列号+填快递单)、归还、搜索 |\n| 🔄 转寄推荐 | 保留原有功能 |\n\n**新增核心文件**:\n- `src/main/device-store.ts` — JSON 持久化层(devices.json + orders.json)\n- `src/renderer/components/Dashboard.tsx` — 仪表盘\n- `src/renderer/components/DeviceInventory.tsx` — 设备库存\n- `src/renderer/components/OrderPanel.tsx` — 订单管理(含发货弹窗)\n- `src/renderer/components/NavTabs.tsx` — 导航标签栏\n\n**数据模型**:\n- Device:序列号、型号、状态(idle/renting)、入库日期\n- Order:客户信息、型号、序列号、快递单号、发货日、起租日、到期日、状态(pending/dispatched/returned)\n\n**工作流**:WPS 下载 Excel → 点「读取数据」→ 自动导入今日订单(去重)+ 转寄匹配 → 发货(选设备+填快递单)→ 归还\n\n### 第三阶段:多人协作 Web 版\n\n在 Electron 主进程中嵌入 Express 服务器,实现局域网多人同时访问。\n\n**架构**:\n```\nElectron 窗口 ←──IPC──→ deviceStore (JSON)\n局域网浏览器 ←──HTTP──→ Express Server → deviceStore\n```\n\n**新增文件**:\n- `src/main/server.ts` — Express 服务器(REST API + 静态文件服务)\n- `src/main/api-polyfill.ts` — 浏览器端 `window.electronAPI` polyfill\n\n**关键设计**:React 组件零改动。服务器在 HTML 中注入 polyfill 脚本,用 `fetch()` 模拟 IPC 接口。\n\n**新增依赖**:express, cors, multer\n\n### 第四阶段:细节完善\n\n1. **发货不限型号**:发货弹窗显示所有空闲设备,不按订单型号过滤\n2. **日期解析修复**:`6.5`(6月5日)之前被解析为 `day=50`→无效→返回空,修复为 `day>31` 时除以10\n3. **名字提取增强**:新增三种格式匹配,兜底用地址前缀/手机号代替,不再丢弃无名字行(从 660 行丢到 0 行丢弃)\n4. **工具栏重构**:操作按钮移到右上角全局工具栏,NavTabs 左侧 + 按钮右侧\n5. **去重修复**:发货后点「读取数据」不再重复导入(去重条件改为仅判断姓名+手机号)\n6. **归还设备修复**:双路匹配(currentOrderId + serialNumber)确保设备正确回到空闲\n7. **订单筛选**:全部/待发货/今天发货/已发订单/已归还,带数量统计\n8. **订单搜索**:支持按客户姓名或手机号搜索\n9. **设备删除**:空闲设备可删除(租用中不可删)\n10. **仪表盘租用中可点击**:弹出窗口显示租用订单列表,可直接点归还\n11. **网页版 URL 显示**:工具栏显示 🌐 网页版地址,一键打开浏览器\n\n### 第五阶段:名称变更 + 打包 + 细节优化\n\n1. **应用更名**:从「租赁客户转寄助手」改为「仓库管理助手」\n2. **删除共享表格按钮**:不再需要单独打开 WPS 链接\n3. **自动读取最新表格**:不再限制文件名,扫描 data 目录下最新的 .xlsx/.xls 文件\n4. **📂 打开文件夹按钮**:一键打开数据文件目录,方便用户放入表格\n5. **Polyfill 补全**:网页版补上 deleteDevice、getRentingOrders、openWebUrl 等方法\n6. **设备删除**:空闲设备可删除(租用中不可删),含确认弹窗\n7. **打包输出**:`仓库管理助手 Setup 1.0.0.exe`(78MB)\n\n### 2026-06-05 下午:多项优化与修复\n\n1. **网页版局域网 IP** — 工具栏动态显示 `http://192.168.x.x:3000/`,一键打开浏览器\n2. **订单日期选择器** — 📅 按任意日期筛选发货订单 + 📦「今天发货」快捷按钮\n3. **全量导入 + 去重优化** — 导入全部客户,去重 key 为 `姓名|电话|发货日|到期日`,Set 去重\n4. **分页** — 订单/设备每页 10 条,切换筛选自动回第 1 页\n5. **日期解析修复** — `raw: false` 读取格式化文本,修复 `6.2`→6月20日的 Bug\n6. **开发模式启动** — 清除 `ELECTRON_RUN_AS_NODE` 环境变量才能正常启动\n7. **平台 + 客服 + 备注** — 读取 Excel 的「平台」「客服」「备注」列并在卡片显示\n8. **iOS 分段控件** — 筛选栏改为圆角滑块风格\n9. **发货信息 Tab** — 已发货/已归还订单记录;搜索支持姓名/手机号/客服/序列号/快递单号\n10. **转寄流程** — 发货信息可转寄,转寄推荐可确认转寄;设备保持 renting,归还才回空闲\n11. **转寄序列号自动查找** — 从发货记录自动填入序列号,找不到时手动输入\n12. **代码简化** — `normalizeDate` 删数字分支,`extractNamePhone` 从 5 种正则简化为 4 步\n13. **设备库存型号搜索** — 搜索框按型号筛选设备\n\n## 运行命令\n\n```bash\nnpm install # 安装依赖\nnpm run build # 构建\nnpm run package # 打包 Windows .exe\n```\n\n### 开发模式启动\n\n**重要**:启动前必须清除 `ELECTRON_RUN_AS_NODE` 环境变量,否则 Electron 会以纯 Node.js 模式运行,导致 `require('electron')` 解析为 npm 包的路径字符串而非 Electron API,应用将崩溃。\n\n**Windows PowerShell**:\n```powershell\n$env:ELECTRON_RUN_AS_NODE=\"\"\nnpm run dev\n```\n\n**Windows CMD**:\n```cmd\nset ELECTRON_RUN_AS_NODE=\nnpm run dev\n```\n\n**Git Bash / MSYS2**:\n```bash\nexport ELECTRON_RUN_AS_NODE=\nnpm run dev\n```\n\n> **故障排查**:如果启动时出现 `TypeError: Cannot read properties of undefined (reading 'whenReady')`,说明 `ELECTRON_RUN_AS_NODE` 未清除。执行 `echo $ELECTRON_RUN_AS_NODE`(或 `echo %ELECTRON_RUN_AS_NODE%`)检查其值,若为 `1` 则需按上述步骤清除。\n\n### 2026-06-06 上午:发货信息、转寄流程、代码优化\n\n1. **「客服」字段** — 读取 Excel「客服」列,订单卡片和发货信息中显示,搜索支持\n2. **「备注」字段** — 读取 Excel「备注」列,卡片地址下方 📝 橙色显示\n3. **设备型号搜索** — 设备库存新增型号搜索框,汇总行显示各型号数量\n4. **发货信息完善** — 显示已发货+已归还订单(完整记录),已归还无操作按钮,显示归还日期\n5. **转寄数据修正** — 来源订单转寄后 status='returned' + returnDate,设备保持 renting\n6. **转寄推荐搜索** — 新增搜索栏按姓名/手机号筛选,已转寄客户显示绿色「已转寄」标签\n7. **今日到期搜索** — 侧边栏新增搜索框(姓名/电话/地址/型号),已转寄显示绿色 ✓ 标记\n8. **代码简化** — normalizeDate 删数字分支,extractNamePhone 5→4 步,RentalEnd 日志精简\n\n### 构建产物\n\n- 开发模式下 Dev Server 运行在 `http://localhost:5173/`\n- Web 服务器运行在 `http://<局域网IP>:3000/`(其他设备可通过局域网访问)\n- 打包输出在 `release/` 目录\n\n## 项目文件结构\n\n```\nRJKF/\n├── src/\n│ ├── main/ # Electron 主进程\n│ │ ├── index.ts # 窗口创建、IPC注册、启动Web服务器\n│ │ ├── wps-service.ts # Excel读取、日期解析、客户解析\n│ │ ├── matcher.ts # 到期筛选、转寄匹配\n│ │ ├── address-parser.ts # 地址解析(省市区街道)\n│ │ ├── ipc-handlers.ts # IPC通信桥接\n│ │ ├── device-store.ts # 设备/订单 JSON 持久化层\n│ │ ├── server.ts # Express Web 服务器 + REST API\n│ │ └── api-polyfill.ts # 浏览器端 window.electronAPI polyfill\n│ ├── preload/\n│ │ └── index.ts # 预加载脚本(Electron ↔ 渲染进程桥接)\n│ ├── renderer/ # React 前端\n│ │ ├── App.tsx # 根组件(状态管理、导航、工具栏)\n│ │ ├── components/\n│ │ │ ├── NavTabs.tsx # 导航标签栏\n│ │ │ ├── Dashboard.tsx # 仪表盘(库存概览+今日动态+租用中弹窗)\n│ │ │ ├── DeviceInventory.tsx # 设备库存(手动/批量入库、筛选、删除)\n│ │ │ ├── OrderPanel.tsx # 订单管理(发货弹窗、搜索、筛选、归还)\n│ │ │ ├── ForwardingList.tsx # 转寄推荐列表\n│ │ │ ├── Sidebar.tsx # 到期客户侧边栏\n│ │ │ ├── MainContent.tsx # 转寄主内容区\n│ │ │ ├── StatsRow.tsx # 统计行组件\n│ │ │ ├── TitleBar.tsx # 自定义标题栏\n│ │ │ └── StatusBar.tsx # 底部状态栏\n│ │ ├── types/ # 渲染进程类型\n│ │ └── assets/ # CSS 样式\n│ └── types/ # 共享类型定义\n├── data/ # 数据文件目录(Excel + JSON)\n├── release/ # 打包输出\n├── electron-vite.config.ts # 构建配置\n└── package.json # 项目配置+打包配置\n```\n\n---\n\n## 2026-06-10:产品重新定位 — 从桌面工具到 SaaS 平台\n\n### 背景\n\n当前软件解决了租赁商户\"筛选到期客户 → 推荐转寄 → 设备/订单管理\"的流程,但整个工作流依然有三大断点:\n\n1. **进单靠手抄**:各平台订单需要人工录入表格,再导入软件\n2. **发货通知靠手工**:发货后需手动把单号+序列号复制到微信群\n3. **数据孤岛**:本地 JSON 文件存储,无法多人协作\n\n### 确定方案:截图 AI 录单 + 订阅制 SaaS\n\n#### 核心产品定位\n\n**仓库管理助手 v2**:面向租赁商户的一站式订单管理平台。最大卖点——截图即可录单,不挑平台,不用 API。\n\n#### 整体架构\n\n```\n┌─────────────────────────────────────────────────┐\n│ 阿里云硅谷 ECS (2核4G) │\n│ │\n│ ┌──────────────────┐ ┌──────────────────────┐ │\n│ │ 用户系统 │ │ 数据服务 │ │\n│ │ - 注册/登录(JWT) │ │ - PostgreSQL │ │\n│ │ - 订阅验证 │ │ - 订单/设备/用户数据 │ │\n│ │ - 免费/Pro/团队 │ │ - 操作日志 │ │\n│ └──────────────────┘ └──────────────────────┘ │\n│ │\n│ ┌──────────────────┐ ┌──────────────────────┐ │\n│ │ Webhook 引擎 │ │ AI 代理(备选) │ │\n│ │ - 企业微信通知 │ │ - 截图OCR中转 │ │\n│ │ - 发货自动推送 │ │ - 限流/计费 │ │\n│ └──────────────────┘ └──────────────────────┘ │\n└──────────────┬──────────────────────────────────┘\n │ HTTPS\n ┌──────────┼──────────────┐\n ▼ ▼ ▼\n 桌面端 App 手机浏览器 未来:微信小程序\n (Electron) (移动端Web) \n Win + Mac \n │\n ├─ 截图 OCR 直连 AI 服务商(不经过服务器)\n │ GPT-4o-mini / 通义千问 VL\n │\n ├─ 本地离线模式(断网也能用基础功能)\n │\n └─ 数据自动同步到云端\n```\n\n#### AI 成本核算\n\n| 项目 | 数值 |\n|------|------|\n| 每张截图 token 量 | ~1500 tokens(含图片+提示词) |\n| GPT-4o-mini 单价 | $0.15 / 1M input tokens ≈ ¥0.0015/张 |\n| 月均 1500 单 | 约 ¥2.25/月 |\n| 备选:通义千问 VL | 约 ¥4.5/月 |\n\n结论:AI 成本可忽略不计,不存在定价障碍。\n\n#### 订阅定价\n\n| 版本 | 价格 | 功能 |\n|------|------|------|\n| 免费版 | ¥0 | 每日 5 次截图 OCR,基础设备/订单管理 |\n| 专业版 | ¥19/月 或 ¥199/年 | 无限截图 OCR,云端备份,多客服账号,全部功能 |\n| 团队版 | ¥39/月 | 专业版全部 + 企业微信通知 + 权限管理 |\n\n盈亏平衡:3 个专业版用户即可覆盖服务器 + AI 成本。\n\n#### 技术选型\n\n| 层 | 技术 |\n|----|------|\n| 桌面端 | Electron + React + TypeScript(保持不变) |\n| 服务器 | Node.js + Express + PostgreSQL |\n| AI 视觉 | GPT-4o-mini(默认)/ 通义千问 VL(国内备选) |\n| 认证 | JWT + bcrypt |\n| 消息推送 | 企业微信 Webhook |\n| 部署 | 阿里云硅谷 ECS + Nginx 反向代理 + Docker |\n\n---\n\n### 开发路线图\n\n#### 第一阶段:截图 AI 录单(优先级 🥇)\n**目标**:彻底消灭手工录表\n\n- [x] 需求讨论与方案敲定(2026-06-10)\n- [ ] 新增「截图录单」页面组件\n- [ ] 实现拖拽/粘贴截图功能\n- [ ] 集成 AI Vision API(GPT-4o-mini)\n- [ ] 结构提取 → 自动填单 → 人工确认 → 保存\n- [ ] 多图批量识别(一次拖入多张截图)\n\n#### 第二阶段:订阅 + 云端后端(优先级 🥈)\n**目标**:实现商业化闭环\n\n- [ ] 阿里云服务器环境搭建(Node.js + PostgreSQL + Nginx)\n- [ ] 用户系统:注册/登录/JWT 鉴权\n- [ ] 订阅系统:免费/Pro/团队三级\n- [ ] 订单数据云同步 API\n- [ ] 桌面端:登录页面 + 订阅状态检查\n\n#### 第三阶段:发货自动化(优先级 🥉)\n**目标**:发货通知 + 一键回传\n\n- [ ] 企业微信 Webhook 集成(发货自动发群消息)\n- [ ] 发货后「一键复制」格式化文本(快递单号+序列号)\n- [ ] 移动端 Web 适配(手机浏览器录单、查看订单)\n- [ ] 截图 OCR 移动端支持(手机截图 → 上传 → 识别)\n\n#### 第四阶段:协作与扩展(远期)\n**目标**:多商户 SaaS 平台\n\n- [ ] 多客服账号 + 角色权限\n- [ ] 数据看板(日/周/月报表)\n- [ ] 微信小程序\n- [ ] 多商户隔离(每个商户独立数据空间)\n\n---\n\n### 本次迭代已完成改动(2026-06-10)\n\n#### macOS 适配\n- `package.json`:新增 `package:mac` 脚本 + dmg/zip 构建目标\n- `src/main/index.ts`:`titleBarStyle: 'hiddenInset'` 启用原生交通灯\n- `src/renderer/components/TitleBar.tsx`:macOS 下隐藏自定义按钮,标题左移 70px\n- `src/main/ipc-handlers.ts`:`path.dirname()` 替代硬编码 `\\\\` 路径分割\n- `src/renderer/assets/global.css`:新增 `@keyframes pulse` 呼吸灯动画\n\n#### 自动同步\n- `src/renderer/App.tsx`:新增 `silentFetch()` + 60 秒轮询 + 开关按钮\n- `src/renderer/components/StatusBar.tsx`:新增同步状态指示灯\n\n#### WPS 宏一键同步\n- `resources/wps-sync-macro.js`:WPS JS 宏,优先 HTTP POST 到本机,降级保存 xlsx\n- `src/main/server.ts`:新增 `POST /api/customers/sync` 端点\n- `src/main/wps-service.ts`:导出 `parseCustomerData` 供端点复用\n\n---\n\n### 待解决问题\n\n1. AI 截图 OCR 的 API Key 管理:用户自带 Key 还是内置在订阅费里?\n → 初步决定:内置在订阅费里,避免用户额外配置\n2. 服务器部署地区:硅谷 → 国内延迟约 180ms,截图 OCR 走客户端直连 AI 服务商,不受影响\n3. 离线模式:桌面端断网时能否使用基础功能(设备管理、本地订单)\n → 保留本地 JSON 存储作为离线缓存,联网后自动同步\n\n---\n\n### 运行命令(更新)\n\n```bash\nnpm install # 安装依赖\nnpm run dev # 开发模式(Windows需先清除ELECTRON_RUN_AS_NODE)\nnpm run build # 构建\nnpm run package:win # 打包 Windows exe\nnpm run package:mac # 打包 macOS dmg\nnpm run package # 同时打包 Win + Mac\n```\n\n---\n\n## 2026-06-10 下午:第一阶段 — 截图 AI 录单功能实现\n\n### 新增文件\n\n| 文件 | 用途 |\n|------|------|\n| `src/main/ocr-service.ts` | AI Vision API 集成,调用 GPT-4o-mini 从截图中提取订单信息 |\n| `src/renderer/components/ScreenshotOrder.tsx` | 截图上传 + AI 识别 + 可编辑表单 + 一键保存 |\n\n### 修改文件\n\n| 文件 | 改动 |\n|------|------|\n| `src/main/ipc-handlers.ts` | 新增 `extract-order-from-image` 和 `create-full-order` IPC 通道 |\n| `src/main/device-store.ts` | 新增 `createFullOrder()`,支持完整订单字段(平台/客服/备注/租期) |\n| `src/preload/index.ts` | 暴露 `extractOrderFromImage` 和 `createFullOrder` 到渲染进程 |\n| `src/renderer/global.d.ts` | 新增对应 TypeScript 类型声明 |\n| `src/renderer/components/NavTabs.tsx` | 新增 📸 截图录单 Tab |\n| `src/renderer/App.tsx` | 引入 ScreenshotOrder 组件并路由到新 Tab |\n| `src/renderer/assets/global.css` | 新增拖拽上传区和表单输入框样式 |\n\n### 截图录单工作流\n\n```\n任意平台订单 → 截图(或 Ctrl+V 粘贴)\n │\n ▼\n软件识别区域展示截图预览\n │\n ▼\n点击「开始识别」→ GPT-4o-mini Vision API\n │\n ▼\nAI 返回结构化数据 → 自动填入表单(标注 🤖 AI 识别)\n │\n ├─ 客户姓名*、手机号*、地址\n ├─ 设备型号、发货日、起租日、到期日\n └─ 平台、客服、备注\n │\n ▼\n人工校对 → 点击「确认保存订单」\n │\n ▼\n订单写入系统 → 2 秒后自动清空,准备下一单\n```\n\n### AI 服务配置\n\n- 模型:GPT-4o-mini(`detail: low` 模式,节省 token)\n- API Key:通过环境变量 `OPENAI_API_KEY` 配置\n- 每张截图约 1500 tokens,成本约 ¥0.0015\n- 系统提示词已优化为中文订单场景,要求严格 JSON 返回\n- 支持 Markdown 代码块容错解析\n\n### 截图方式\n\n- 拖拽图片到上传区\n- 点击上传区选择文件\n- Ctrl+V 粘贴剪贴板中的截图(全局监听,无需聚焦上传区)\n\n---\n\n## 2026-06-10 下午:第二阶段 — 订阅制 + 阿里云后端\n\n### 新增项目:server/\n\n独立的云端服务,部署在阿里云硅谷 ECS。\n\n```\nserver/\n├── package.json\n├── tsconfig.json\n├── Dockerfile\n├── docker-compose.yml\n├── src/\n│ ├── index.ts # Express 入口,端口 3001\n│ ├── db.ts # PostgreSQL 连接 + 表初始化\n│ ├── db-init.ts # 首次部署:创建数据库 + 建表\n│ ├── middleware/\n│ │ └── auth.ts # JWT 鉴权 + 订阅等级中间件\n│ └── routes/\n│ ├── auth.ts # 注册 / 登录 / 验证 Token\n│ └── sync.ts # 订单 + 设备云端同步(upsert)\n```\n\n### 数据库设计(PostgreSQL)\n\n| 表 | 用途 | 关键字段 |\n|----|------|----------|\n| `users` | 用户账号 | email, password_hash, subscription_tier, subscription_expires |\n| `orders` | 订单数据 | 完整订单字段,user_id 关联用户 |\n| `devices` | 设备库存 | serial_number, status, user_id 关联用户 |\n\n同步策略:`ON CONFLICT ... DO UPDATE`(upsert),以客户端生成的 ID 为主键。\n\n### 新增桌面端文件\n\n| 文件 | 用途 |\n|------|------|\n| `src/renderer/components/LoginPage.tsx` | 登录/注册页面,支持离线模式 |\n| `src/renderer/services/api-client.ts` | HTTP 客户端,封装 auth + sync API |\n| `src/renderer/services/sync-service.ts` | 后台云同步(30 秒间隔推送) |\n\n### 修改桌面端文件\n\n| 文件 | 改动 |\n|------|------|\n| `src/renderer/App.tsx` | 新增 auth gate:未登录显示 LoginPage,已登录显示主应用 |\n\n### 用户流程\n\n```\n启动 App\n │\n ▼\n检查本地 Token → 服务器验证\n │\n ├─ 有效 → 进入主界面(订阅等级:free/pro/team)\n │\n ├─ 无效 → 显示登录页\n │ ├─ 注册 / 登录 → 进入主界面\n │ └─ 离线使用 → 进入主界面(数据仅本地)\n │\n ▼\n主界面:每 30 秒自动推送订单/设备到云端\n\n### 定价对应功能限制\n\n| 版本 | OCR 次数 | 多客服 | 云同步 | 企业微信通知 |\n|------|----------|--------|--------|-------------|\n| Free | 5次/天 | ❌ | ❌ | ❌ |\n| Pro | 无限 | ✅ | ✅ | ❌ |\n| Team | 无限 | ✅ | ✅ | ✅ |\n\n### 部署到阿里云\n\n```bash\n# 在服务器上\ncd server\nnpm install\nnpm run build\n\n# 设置环境变量\nexport DB_HOST=localhost\nexport DB_PASSWORD=your-password\nexport JWT_SECRET=your-random-secret\n\n# 初始化数据库\nnpm run db:init\n\n# 启动\nnpm start\n\n# 或使用 Docker\ndocker-compose up -d\n```\n\n### 桌面端连接服务器\n\n在 `src/renderer/services/api-client.ts` 中修改 `API_BASE` 为服务器地址:\n\n```typescript\nconst API_BASE = 'https://your-server.com:3001/api'\n```\n\n---\n\n## 2026-06-10 下午:第三阶段 — 企业微信通知 + 移动端 + 一键复制\n\n### 新增文件\n\n| 文件 | 用途 |\n|------|------|\n| `src/main/wecom-service.ts` | 企业微信 Webhook 通知服务(发货/转寄自动推送群消息) |\n| `src/renderer/components/WecomSettings.tsx` | 设置页面:Webhook URL 配置 + 开关 |\n| `src/renderer/mobile.html` | 移动端 Web 页面(订单管理 + 发货 + 截图上传) |\n\n### 修改文件\n\n| 文件 | 改动 |\n|------|------|\n| `src/main/ipc-handlers.ts` | 新增 4 个 IPC 通道(wecom 配置读写 + 通知触发);发货/转寄自动调 Webhook |\n| `src/renderer/components/NavTabs.tsx` | 新增 ⚙️ 设置 Tab |\n| `src/renderer/App.tsx` | 引入 WecomSettings 组件 |\n| `src/renderer/components/OrderPanel.tsx` | 发货成功后显示绿色通知条 + 📋 一键复制按钮 |\n| `src/preload/index.ts` | 暴露 getWecomConfig / saveWecomConfig |\n| `src/renderer/global.d.ts` | 新增类型声明 |\n| `src/main/server.ts` | 新增 `/mobile` 路由,服务移动端页面 |\n\n### 企业微信通知\n\n**配置方式**:\n1. 企业微信群 → 群设置 → 群机器人 → 添加\n2. 复制 Webhook 地址(`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...`)\n3. 打开软件 → ⚙️ 设置 → 粘贴地址 → 启用 → 保存\n\n**通知时机**:\n- 发货确认后 → 群内自动推送 📦 发货通知(客户名、序列号、快递单号、客服)\n- 转寄确认后 → 群内自动推送 🔄 转寄通知\n\n**格式示例**:\n```\n📦 发货通知\n\n客户:张三\n电话:13812345678\n设备:DJI Osmo Pocket 3\n序列号:4B6RLxxxxxxxx\n快递单号:SF1234567890\n平台:淘宝\n客服:小王\n```\n\n### 移动端 Web\n\n访问地址:`http://<电脑IP>:3000/mobile`\n\n功能:\n- 📋 订单列表(筛选待发/已发,搜索,发货,归还)\n- 📮 已发货记录\n- 📸 截图上传(拍照或选图,上传后桌面端确认)\n- 📊 库存概览(订单数、设备数、空闲/租用统计)\n- 每 30 秒自动刷新数据\n\n### 一键复制\n\n发货成功后,订单管理页顶部显示绿色通知条:\n`✅ 张三 发货成功 · Pocket 3 · 4B6RLxxx · SF1234567890 [📋 复制]`\n\n点击「复制」按钮,自动复制格式化为:\n```\n发货信息:\n客户:张三\n设备:DJI Osmo Pocket 3\n序列号:4B6RLxxxxxxxx\n快递单号:SF1234567890\n```\n\n直接粘贴到平台或微信群即可。10 秒后通知条自动消失。\n\n---\n\n## 第二阶段:云端化与账户系统(2026-06-15 ~ 2026-06-16)\n\n### 云端服务器部署\n\n- **服务器**:阿里云 ECS(美国硅谷,IP `47.254.36.2`)\n- **系统**:Alibaba Cloud Linux 3(OpenAnolis Edition)\n- **部署方式**:Docker + Docker Compose\n- **组件**:\n - PostgreSQL 16(Alpine)\n - Node.js 20 + Express API 服务(端口 3001)\n- **安装过程**:\n - 阿里云 Linux 不在 Docker 官方安装脚本支持列表中,手动通过 dnf 添加 Docker CE 仓库安装\n - Docker 26.1.3 + Docker Compose v2.27.0\n - 安全组开放 3001 端口\n\n### 账户系统\n\n- **数据库表**:`users`(id, email, password_hash, subscription_tier, subscription_expires, extra_credits, created_at)\n- **API 路由**:\n - `POST /api/auth/register` — 注册\n - `POST /api/auth/login` — 登录(返回 JWT Token)\n - `GET /api/auth/me` — 验证 Token,返回用户信息\n- **密码加密**:bcryptjs(10 轮哈希)\n- **鉴权方式**:JWT(30 天有效期),Bearer Token\n\n### 数据同步\n\n- **同步表**:`orders`、`devices`\n- **API 路由**:\n - `GET/POST /api/sync/orders` — 订单拉取/推送\n - `GET/POST /api/sync/devices` — 设备拉取/推送\n- **客户端同步**:每 30 秒自动推送本地数据到云端\n\n### 客户端修复\n\n- 修复 API 路径缺少 `/api` 前缀导致 `Failed to fetch`\n- 修改默认服务器地址为 `http://47.254.36.2:3001`\n- 注册 `get-wecom-config` / `save-wecom-config` IPC 处理器\n\n### 个人信息与版本系统(2026-06-15)\n\n- **主页用户栏**:左上角显示头像圆圈(邮箱首字母)+ 邮箱 + 版本标签\n- **退出登录**:主页右侧退出按钮,清除 Token 回到登录页\n- **版本重命名**:\n - 免费版 → 免费版\n - 团队版 → **Pro+版**(¥29/月)\n - 专业版 → **Plus版**(¥59/月)\n- **设置页面**:版本特权三栏对比 + 升级入口\n\n### 激活码系统\n\n- **数据库表**:`activation_codes`(code, tier, duration_days, used_by, used_at)\n- **API 路由**:\n - `POST /api/activation/admin/generate` — 管理员生成激活码\n - `POST /api/activation/admin/list` — 查看激活码列表\n - `POST /api/activation/redeem` — 用户兑换激活码\n- **管理员密钥**:`rjkf-admin-2024`(通过 `.env` 的 `ADMIN_SECRET` 配置)\n- **管理员面板**:输入密钥验证 → 选择版本/天数/数量 → 生成激活码 → 查看记录\n- **隐藏入口**:快速点击首页「仓库管理助手」标题 3 次进入\n- **客户端**:设置页「🎫 激活码兑换」输入框\n\n### AI 识别次数系统\n\n- **数据库表**:`ai_usage`(user_id, period_key, count, extra_used)\n- **配额规则**:\n - 免费版:每日 5 次\n - Pro+版:每月 1500 次\n - Plus版:不限\n- **客户端显示**:\n - AI 截图录单页面:识别按钮上方显示「本月/今日剩余 X / N 次」\n - 主页用户栏:显示「🤖 今日/本月剩余 X 次」\n - 次数用完变红色,按钮禁用\n\n### 次数充值系统\n\n- **数据库表**:`recharge_codes`(code, credits, used_by, used_at)\n- **API 路由**:\n - `POST /api/ai-usage/recharge/generate` — 管理员生成充值码\n - `POST /api/ai-usage/recharge/redeem` — 用户兑换充值码\n - `GET /api/ai-usage/remaining` — 查询剩余次数\n - `POST /api/ai-usage/increment` — 使用后+1\n- **管理员面板**:充值码标签页,可选 50/100/200/500/1000 次\n- **客户端**:设置页「⚡ 次数充值」输入框\n\n### 注册流程简化\n\n- 初期添加了邮箱验证码功能(`verification_codes` 表 + nodemailer SMTP)\n- 后续移除验证码要求,注册仅需邮箱 + 密码\n- 移除登录页「离线使用」入口,必须联网使用\n\n### 自动登录\n\n- 修改 `verifyToken()` 逻辑:服务器不可达时使用缓存 Token 自动登录\n- 重启应用自动进入主页,无需重复输入账号密码\n\n---\n\n## 第三阶段:团队协作功能(2026-06-16)\n\n### 好友系统\n\n- **数据库表**:`friends`(user_id, friend_id, status: pending/accepted/rejected)\n- **API 路由**:\n - `POST /api/friends/request` — 发送好友请求(按邮箱查找)\n - `GET /api/friends/requests` — 查看待处理请求\n - `POST /api/friends/accept` — 接受请求\n - `POST /api/friends/reject` — 拒绝请求\n - `GET /api/friends/list` — 好友列表\n - `DELETE /api/friends/:friendId` — 删除好友\n- **权限控制**:仅 Pro+ 及以上版本可用(`requireTier('team', 'pro')` 中间件)\n- **客户端**:好友协作页面(添加好友 / 请求列表 / 好友管理)\n\n### 订单协作\n\n- **数据库表**:`order_collaborators`(order_id, user_id, added_by)\n- **API 路由**:\n - `POST /api/collab/orders/:id/share` — 分享订单给好友\n - `GET /api/collab/orders/:id/collaborators` — 查看协作者\n - `GET /api/collab/shared-with-me` — 查看分享给我的订单\n - `DELETE /api/collab/orders/:id/share/:userId` — 移除协作者\n- **客户端**:发货信息页每个订单新增「分享」按钮,弹出好友列表选择\n\n### 企业绑定\n\n- **数据库表**:\n - `enterprises`(name, owner_id, invite_code)\n - `enterprise_members`(enterprise_id, user_id, role: admin/member)\n- **API 路由**:\n - `POST /api/enterprise/create` — 创建企业(Pro+ 以上)\n - `POST /api/enterprise/join` — 通过邀请码加入\n - `GET /api/enterprise/my` — 我的企业信息\n - `GET /api/enterprise/members` — 成员列表\n - `POST /api/enterprise/kick` — 管理员移除成员\n - `POST /api/enterprise/leave` — 退出/解散企业\n - `POST /api/enterprise/regenerate-code` — 刷新邀请码\n - `GET /api/enterprise/orders` — 企业内所有订单\n- **客户端**:\n - 企业绑定页面:创建企业/输入邀请码加入\n - 企业详情:成员列表、邀请码显示/刷新、移除成员、退出\n - 同企业成员自动可见彼此订单\n\n### 逾期订单追踪(2026-06-16)\n\n- **判断逻辑**:到期日后超过 2 天未点「归还」 → 标记为「⚠️ 已逾期」\n- **仪表盘**:新增「已逾期」统计卡片,点击弹出逾期订单详情列表\n- **逾期详情**:显示客户名、电话、设备、到期日、已逾期天数、可直接点归还\n- **主页**:统计栏新增「已逾期」数字(红色),点击跳转仪表盘\n- **发货信息**:逾期订单红色边框 + 「⚠️ 已逾期」标签\n\n---\n\n## 第四阶段:API Key 管理与打包(2026-06-22)\n\n### OCR API Key 演进\n\n- 初期使用 API2D 中转 Key(`fk244173-...`),硬编码在代码中\n- API2D Key 过期后,改为阿里云 DashScope Key(`sk-ws-...`)\n- 最终方案:硬编码为系统管理,客户不可更改\n- OCR 接口检测逻辑:\n - `sk-` 开头 → DashScope(`dashscope.aliyuncs.com`)\n - `fk` 开头 → API2D(`oa.api2d.net`)\n - 其他 → OpenAI 官方\n\n### Windows 打包\n\n- 使用 `electron-builder` 生成 NSIS 安装包\n- 输出文件:`release/仓库管理助手 Setup 1.0.0.exe`(~78MB)\n- 安装选项:可自定义安装目录,创建桌面快捷方式\n- 跨平台构建:macOS 上通过 Wine 交叉编译 Windows 包\n- 下载依赖:Electron v31.7.7 win32-x64 + winCodeSign + NSIS 3.0.4.1\n\n### 服务器重置恢复(2026-06-22)\n\n- 服务器被重置后完整重新部署:\n - SSH 密钥清理 + 重新授权\n - 安装 Docker Compose v5.1.4\n - 上传代码 → 构建镜像 → 启动服务\n - 数据库初始化(全新空库)\n - 安全组端口 3001 重新开放\n\n---\n\n## 当前系统架构\n\n```\n┌──────────────────────────────────────────────┐\n│ 客户端(Electron) │\n│ React + TypeScript + Vite │\n│ ┌──────────┬──────────┬──────────────┐ │\n│ │ 主页 │ AI截图 │ 订单/设备管理 │ │\n│ │ 仪表盘 │ 好友协作 │ 企业绑定 │ │\n│ │ 发货信息 │ 设置 │ 管理员面板 │ │\n│ └──────────┴──────────┴──────────────┘ │\n│ │ API Client (fetch + JWT) │\n└──────────┼────────────────────────────────────┘\n │ HTTPS\n┌──────────┼────────────────────────────────────┐\n│ 阿里云 ECS(47.254.36.2) │\n│ ┌─────────────────────────────┐ │\n│ │ Express API(端口 3001) │ │\n│ │ ├─ auth (注册/登录/JWT) │ │\n│ │ ├─ sync (订单/设备同步) │ │\n│ │ ├─ subscription (版本) │ │\n│ │ ├─ activation (激活码) │ │\n│ │ ├─ ai-usage (次数管理) │ │\n│ │ ├─ friends (好友) │ │\n│ │ ├─ collab (订单协作) │ │\n│ │ ├─ enterprise (企业) │ │\n│ │ └─ verify (邮箱验证) │ │\n│ └──────────┬──────────────────┘ │\n│ ┌──────────┴──────────────────┐ │\n│ │ PostgreSQL 16(端口 5432) │ │\n│ └─────────────────────────────┘ │\n└───────────────────────────────────────────────┘\n```\n\n## 数据库表总览\n\n| 表名 | 用途 |\n|------|------|\n| `users` | 用户账户(含 tier、到期、充值次数) |\n| `orders` | 租赁订单(含设备、快递、租期、状态) |\n| `devices` | 设备库存(序列号、状态) |\n| `activation_codes` | 版本激活码(一次性使用) |\n| `recharge_codes` | AI 次数充值码(一次性使用) |\n| `ai_usage` | AI 识别用量(按用户+周期统计) |\n| `verification_codes` | 邮箱验证码(5 分钟有效) |\n| `friends` | 好友关系(pending/accepted/rejected) |\n| `order_collaborators` | 订单协作者 |\n| `enterprises` | 企业信息 |\n| `enterprise_members` | 企业成员 |\n\n## 功能权限矩阵\n\n| 功能 | 免费版 | Pro+版 | Plus版 |\n|------|--------|--------|--------|\n| 基础订单管理 | ✅ | ✅ | ✅ |\n| 设备库存管理 | ✅ | ✅ | ✅ |\n| Excel 表格导入 | ✅ | ✅ | ✅ |\n| AI 截图录单 | 5次/日 | 1500次/月 | 不限 |\n| 云数据同步 | ✅ | ✅ | ✅ |\n| 企业微信通知 | ❌ | ✅ | ✅ |\n| 好友协作 | ❌ | ✅ | ✅ |\n| 企业绑定 | ❌ | ✅ | ✅ |\n| 多客服协作 | ❌ | ❌ | ✅ |\n| API 开放接口 | ❌ | ❌ | ✅ |\n"}}}} |