Files

1 line
58 KiB
JSON
Raw Permalink 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.
{"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"}}}}