# swupload **Repository Path**: xuting/swupload ## Basic Information - **Project Name**: swupload - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-31 - **Last Updated**: 2026-09-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 退税资料上传系统(swupload / tax-refund-materials) 面向出口外贸企业的**出口退税备案单证管理系统**。系统对接东松外部统一接口(`SwUploadApi`)完成登录认证与发票数据同步,业务员按「账套 → 发票号」维度逐项上传、替换、下载、删除六类退税备案单证,系统自动判定资料是否齐全;管理员可配置同步周期、外部接口地址,并可启停定时同步。 ## 一、功能特性 ### 登录与身份 - **账套登录**:登录时选择账套(不同账套业务员密码可能不同),账套随账号密码一并提交外部接口校验;登录后权限限定为该账套,换账套需重新登录 - **多身份支持**:一个账号密码可能对应多个业务身份(多个 userid)——登录后弹出**身份选择页**,选择进入哪个身份;进入系统后顶栏可**随时切换身份**(切换不重复调用外部接口,发票数据随身份即时刷新) - **内置管理员**:`admin / admin` 不走外部验证,直接以管理员身份进入 - **会话持久化**:浏览器刷新后保持登录(本地仅存身份信息,不存密码);服务端会话失效(如服务重启)自动登出 - **ERP 免登直达**:ERP 发票列表页点「上传」直接进入本系统对应发票的资料页(HMAC-SHA256 签名,详见 3.4) ### 发票与资料 - **发票同步**:外部接口 `method=fplist` 拉取入库;服务启动与定时任务以**全部账套 × 全部业务员(userid=0)**维度自动同步;管理员可手动全量同步;**业务员点「同步 ERP 发票」仅同步当前账套** - **发票表格**:发票号 / 币种 / 出运金额(无货币符号)/ 所属账套 / 报关单号 / 业务员(显示姓名)/ 资料状态(含 n/6 完成度)/ 操作;支持发票号 + 报关单号双搜索、状态筛选、**分页(每页 20/50/100 条)** - **最新同步时间**:工作台同步按钮旁实时显示当前账套数据的最近同步时间 - **六类备案单证**:出口外销合同、国内采购购货合同、提单、配套费用发票、委托报关协议、报关服务费发票;支持**点击选择与拖拽文件两种上传方式**,重复上传自动替换旧文件 - **资料完整度看板**:齐全/待补状态、完成度、统计卡片 - **多账套与身份隔离**:仅能访问登录账套内、归属当前身份(userid)的发票与资料 - **发票账套变更迁移**:发票所属账套变化时,已上传资料(索引 + 物理文件)自动迁移 ### 管理员 - 同步周期(5–1440 分钟)与外部接口地址配置,保存即生效 - **停止 / 恢复周期同步**:停止后定时任务注销(手动同步不受影响);恢复时立即全量同步一次 - 状态区:最近开始 / 最近成功 / **最近同步信息(含各账套张数,持久化)** / 下次自动同步(停止时显示"已停止")/ 最近错误,**每 10 秒自动刷新** - 手动「同步发票(全部业务员)」,完成后显示各账套明细 ## 二、技术栈 | 类别 | 技术 | 版本 | |---|---|---| | 语言 | TypeScript(前后端) | ^5.7.2,strict 模式 | | 运行时 | Node.js | ≥ 18(服务端使用全局 fetch) | | 前端 | React | ^18.3.1 | | 前端构建 | Vite + @vitejs/plugin-react | ^6.0.5 / ^4.3.4 | | 后端 | Express(**5.x**) | ^5.1.0 | | 文件上传 | Multer | ^2.0.0 | | 后端 TS 运行器 | tsx(watch 热重载) | ^4.19.2 | | 并发启动 | concurrently | ^9.1.2 | | 测试 | Vitest + Testing Library + Supertest + jsdom | ^3.0.2 等 | | 数据库 | **无**(本地 JSON 文件 + 文件系统存储) | — | 无需 JDK、无需数据库、无需 Redis/MQ 等中间件。 ## 三、外部接口对接(SwUploadApi) ### 3.1 登录验证(method=login) ```text POST http://10.11.1.224/dongsong/servlet/action.SwUploadApi Content-Type: application/x-www-form-urlencoded method=login&loginname=<账号>&password=<密码>&accountSetId=<账套ID> ``` 返回 JSON 约定(新版多身份): - `{success:true, total:N, msg:"", loginlist:[{userid, showname}, ...]}`——`total` 为身份个数(>0 即校验通过),**一个账号密码可能对应多个 userid**,由业务员选择进入 - `total <= 0` → 登录失败,`msg` 为失败原因(登录页直接展示) - 兼容旧版单对象返回(`total` 即 userid、`msg` 为显示名),自动按单身份处理 - 校验失败时外部系统可能返回 302 重定向(跳回登录页)而非 JSON,同样视为账号密码错误 ### 3.2 发票列表(method=fplist) ```text method=fplist&userid=<用户唯一ID>&accountSetId=<账套ID> ``` - `userid > 0`:只返回该业务员的发票;`userid = 0`:返回**全部业务员**的发票(管理员全量同步用) - 返回 `{success, total, msg, fplist:[...]}`,字段映射: | 外部字段 | 系统字段 | 说明 | |---|---|---| | `customno` | `invoiceNo` | 发票号 | | `bzmc` | `currency` | 币种(如 USD) | | `htmoney` | `amount` | 出运金额 | | `customs_no` | `customsDeclarationNo` | 报关单号 | | `creatorid` | `salespersonId` | 创建人(业务员)ID | | `creator` | `salespersonName` | 业务员姓名(表格显示) | **脏数据归一化**(`server/swUploadInvoiceProvider.ts`,生产库实测存在 null / 首尾空白 / 制表符 / 特殊字符):去首尾空白、不安全字符替换为下划线;`customno` 为空 → 兜底 `CI-`;`customs_no` 为空 → 显示「未登记」;金额非法 → 兜底 0。 ### 3.3 身份与权限模型 - 登录选择账套 → 业务员权限限定为该账套(`accountSetIds = [所选账套]`),发票/资料按所选身份的 `userid` 过滤 - 多身份时登录页选择;系统内顶栏切换身份——**切换与选择均不重复调用外部接口**(登录时签发一次性选择令牌;身份列表缓存在服务端会话中) - `loginname` 命中演示管理员(`admin-01`)时以管理员角色进入 ### 3.4 ERP 免登对接(列表页「上传」直达) ERP 发票列表页的「上传」链接携带签名身份,跳转到本系统后免登录,直接进入链接指定的账套并按 `invoiceNo` 直达该发票的资料上传页。直达匹配顺序:当前账套本地查找 → 未命中自动同步一次再找 → 其他授权账套查找并自动切换 → 都没有才提示。 ```text http://<本系统>/?invoiceNo=<发票号>&userId=<业务员ID>&showname=<姓名> &accountSetId=<账套ID>&expiresAt=<过期时间戳(ms)>&signature= ``` 待签串(HMAC-SHA256,密钥即 `ERP_SSO_SECRET`):`userid|showname|invoiceNo|expiresAt`(未提供账套时)或 `userid|showname|invoiceNo|expiresAt|accountSetId`(提供账套时,账套参与签名防转发越权)。 | 规则 | 说明 | |---|---| | 账套取值 | ERP 侧 `SystemContext.getCompanyID()`,必须与本系统账套 ID 一致(`ds`/`dm`/`dg`) | | 服务端校验顺序 | 有效期(401)→ 签名(401)→ 账套存在(400)→ 账套权限(403) | | 参数用后即清 | 免登成功后清除地址栏 `userId`/`showname`/`expiresAt`/`signature`/`accountSetId` | 密钥配置:环境变量 `ERP_SSO_SECRET`,或写入 `storage/erp-sso-secret` 文件;**未配置时免登一律拒绝(503)**。 ## 四、本地部署与启动 ### 4.1 前置环境 - Node.js ≥ 18(建议 20+),npm ≥ 9 - 网络可访问外部接口 `http://10.11.1.224`(同内网 / VPN) ### 4.2 配置 | 配置项 | 位置 | 说明 | |---|---|---| | 登录接口地址 | 环境变量 `EXTERNAL_LOGIN_URL` | 默认 `http://10.11.1.224/dongsong/servlet/action.SwUploadApi` | | 发票数据源地址 | 管理员页「ERP 基础地址」(存于 `storage/sync-settings.json`) | 留空使用与登录相同的默认地址;粘贴带 ?参数 的完整地址会自动清洗 | | ERP 免登密钥 | 环境变量 `ERP_SSO_SECRET` 或 `storage/erp-sso-secret` 文件 | 与 ERP 侧 JSP 配置的密钥一致;未配置则免登不可用 | | API 端口 | 环境变量 `PORT` | 默认 3001;注意 `vite.config.ts` 代理目标写死 3001,需保持一致 | | 数据目录 | 环境变量 `STORAGE_ROOT` | 默认项目下 `storage/`;可指向独立数据盘,测试/多实例隔离时使用 | | 演示账号 / 账套 | `server/demoData.ts`、`src/data/mockErp.ts` | 账套 ID:`ds`(东松医疗)、`dm`(东贸贸易)、`dg`(东贸国际贸易) | ### 4.3 数据初始化与存储位置 无数据库。所有数据以**本地 JSON 文件 + 文件系统**形式持久化。存储根目录**锚定在项目根目录下的 `storage/`**(按代码文件位置定位,与启动目录无关)。 | 文件 / 目录 | 内容 | |---|---| | `storage/invoices.json` | 本地发票库(以 `invoiceNo` 为主键 upsert) | | `storage/sync-settings.json` | 同步周期、开关、外部接口地址、最近同步状态/信息/错误 | | `storage/materials.json` | 上传资料索引 | | `storage/uploads/<账套>/<发票号>/<资料类型>/.<扩展名>` | 上传的附件实体文件 | | `storage/erp-sso-secret` | ERP 免登密钥文件(可选;优先级低于环境变量,勿提交版本库) | | `storage-backups/` | 索引快照(`storage` 同级兄弟目录,每次启动自动快照三个 JSON 索引、保留最近 5 份) | ### 4.4 启动命令 ```bash npm install # 安装依赖 npm run dev # 开发模式:同时启动前端(6475) + API(3001,热重载) npm run build # 生产构建(tsc -b + vite build → dist/) npm start # 生产模式:API(3001) 直接托管前端页面 npm test # 运行前后端单元测试 ``` ### 4.5 访问地址 | 项 | 地址 | |---|---| | 前端页面(开发) | http://localhost:6475 或 http://<本机IP>:6475(已开启局域网访问) | | 前端页面(生产) | http://<服务器IP>:3001(监听全部网卡) | | 健康检查 | http://127.0.0.1:3001/api/health → `{"status":"ok"}` | | 数据诊断 | http://127.0.0.1:3001/api/storage-info | 服务对 IP 无任何硬编码,可直接部署到任意服务器;启动时控制台自动打印本机所有可访问地址。**注意**:其他电脑访问失败通常是防火墙拦截 3001/6475 端口,需放行入站规则。 ## 五、项目结构 ```text swupload/ ├─ public/favicon.svg # 站点图标(深蓝"税"字徽标) ├─ src/ # React 前端 │ ├─ App.tsx # 全部页面:登录(+身份选择) / 工作台(搜索/分页/同步时间) / 资料详情(拖拽上传) / 管理员设置 │ ├─ domain/ # 领域类型与资料齐全判定 │ └─ data/ # API 客户端:loginApi(含身份选择/切换) / ssoApi / invoiceApi / materialApi / adminSyncApi / session │ ├─ server/ # Express 后端 │ ├─ index.ts # 启动入口:依赖装配 / 监听 / 定时同步 / 启动备份 / dist 托管 │ ├─ app.ts # REST 路由与权限校验(登录/身份选择/身份切换/免登/发票/资料/管理员) │ ├─ externalLoginApi.ts # 外部登录(method=login,多身份 loginlist 解析) │ ├─ swUploadInvoiceProvider.ts # 外部发票数据源(method=fplist + 字段映射 + 脏数据归一化) │ ├─ erpSso.ts # ERP 免登 HMAC-SHA256 签名/验签 │ ├─ loginSessionStore.ts # 服务端内存会话(含已验证身份列表,切换身份免调外部接口) │ ├─ fileStore.ts # JSON 索引原子写入(临时文件+替换,EPERM 退避重试) │ ├─ invoiceRepository.ts # storage/invoices.json 读写 │ ├─ materialRepository.ts # 资料索引与文件管理(含账套迁移) │ ├─ syncSettingsRepository.ts # storage/sync-settings.json 读写 │ ├─ invoiceSyncService.ts # 同步核心:校验 / upsert / 资料迁移 │ ├─ syncScheduler.ts # 定时调度(防重入,支持停止/恢复) │ └─ demoData.ts # 演示账号 / 账套(ds/dm/dg) │ └─ storage/ # 运行时数据(已 gitignore,见注意事项 6) ``` ## 六、API 一览 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/login` | 登录:多身份返回 `{requireIdentitySelection, identities, selectionToken}`;单身份直接建立会话 | | POST | `/api/login/select` | 多身份:凭一次性令牌(或账号密码)按所选 userid 建立会话 | | POST | `/api/identity/switch` | 系统内切换身份(服务端缓存身份列表,不调外部接口) | | POST | `/api/sso/login` | ERP 免登:签名身份换取会话(见 3.4) | | GET | `/api/health` | 健康检查(免鉴权) | | GET | `/api/storage-info` | 数据诊断:storage 路径、发票/资料数量、资料所在账套(免鉴权) | | GET | `/api/invoices?accountSetId=` | 查询本地发票(仅本人 + 授权账套) | | POST | `/api/invoices/sync` | 手动同步(业务员=当前账套本人;管理员=userid=0 全部业务员) | | GET | `/api/materials?accountSetId=` | 批量返回当前账套本人全部发票的资料 | | GET | `/api/materials?accountSetId=&invoiceNo=` | 查询单张发票的资料 | | POST | `/api/materials` | 上传/替换资料(multipart,≤20MB,pdf/jpg/png/office 白名单) | | GET | `/api/materials/{id}/file` | 下载文件 | | DELETE | `/api/materials/{id}` | 删除资料(索引 + 物理文件) | | GET/PUT | `/api/admin/sync-settings` | 管理员查看/修改同步周期、地址、周期同步开关 | | POST | `/api/admin/sync-info` | 管理员回写同步摘要(状态区"最近同步信息") | 除登录、免登与健康检查外,接口均需请求头 `X-Demo-User-Id`(登录后由前端自动携带,值为当前身份的 userid)。 ## 七、注意事项与常见问题 1. **会话为内存态**:服务端会话存于进程内存(默认 8 小时),**服务重启后需重新登录**(前端会自动检测 401 并回到登录页);浏览器刷新不影响登录(本地持久化,不含密码)。 2. **切换账套**:登录时选定账套后不能在系统内切换账套(退出重登即可);系统内的"身份"下拉切换的是**同账号下的业务身份(userid)**,与账套是两回事。 3. **上传限制**:单文件 ≤ 20MB(超出返回 413);仅支持 pdf / jpg / jpeg / png / doc / docx / xls / xlsx。 4. **发票号 / 报关单号格式**:同步入库要求仅含字母、数字、下划线、短横线、中文(资料目录路径安全控制);外部脏数据自动归一化。 5. **数据备份(重要)**:索引有启动快照(`storage-backups/`,保留 5 份),但**物理附件不在此范围**。推荐:Windows 计划任务 + robocopy **每日全量备份到共享文件夹、按日期保留 30 天**(备份整个 `storage/` 目录,恢复=停服务拷回+起服务,用 `/api/storage-info` 核对)。 6. **为什么 `storage/` 在 .gitignore 里**:Git 只管源代码;发票数据与涉税附件(体积大、含敏感信息、进历史删不净)不应进代码仓库。**gitignore 不影响系统读写这些文件**,数据安全靠第 5 条的备份方案。 7. **安全提示**:业务接口信任 `X-Demo-User-Id` 请求头、会话按需求保留明文密码字段,属内网演示级设计,**不可直接暴露公网**。 8. **常见报错排查**: - 登录提示「外部登录服务不可用」→ 检查到 `10.11.1.224:8080` 的网络 / `EXTERNAL_LOGIN_URL` - 免登提示 503 → 服务端未配置 `ERP_SSO_SECRET` - 免登提示签名无效 → ERP 侧 JSP 密钥与本系统不一致,或链接超过 30 分钟 - 发票列表为空 → 点「同步 ERP 发票」,或查看管理员页「最近错误」 - 上传的资料"不见了" → 先访问 `/api/storage-info`:`materialCount>0` 但位置前缀是别的账套 → 资料随发票"搬家"了;为 0/null → 检查 storageRoot 与 materials.json 9. **Express 为 5.x**:使用了 `/{*splat}` 等新版路由语法,排障时勿套用 Express 4 资料。