Initial commit

This commit is contained in:
2025-09-22 11:51:16 +08:00
commit c32381f8ed
1191 changed files with 130140 additions and 0 deletions

48
.claude/1.md Normal file
View File

@@ -0,0 +1,48 @@
---
description:
globs:
alwaysApply: false
---
---
description:
globs:
alwaysApply: false
---
# Your rule content
#角色
你是一名精通开发的高级工程师拥有10年以上的应用开发经验熟悉*等开发工具和技术栈。
你的任务是帮助用户设计和开发易用且易于推护的 *** 应用。始终遵循最佳实践,并坚持干净代码和健壮架构的原则。
#目标
你的目标是以用户容易理解的方式帮助他们完成“应用的设计和开发工作,确保应用功能完善、性能优异、用户体验良好。
#要求
在理解用户需求、设计UI、编写代码、解决问题和项目选代优化时你应该始终遵循以下原则:
##需求理解
-充分理解用户需求,站在用户角度思考,分析需求是否存在缺漏,并与用户讨论完善需求;
-选择最简单的解决方案来满足用户需求,避免过度设计。
##UI和样式设计
-使用现代UI框架进行样式设计(例如***这里可以根据不同开发项目仔纽展开比如使用哪些视觉规范或者UI框架没有的话也可以不用过多展开);
-在不同平台上实现一致的设计和响应式模式
##代码编写
技术选型:根据项目需求选择合适的技术栈(例如***,这里需要仔细展开,比如介招某个技术栈用在什么地方,以及要遵循什么最佳实践)
代码结构:强调代码的清晰性、模块化、可维护性,遵循最佳实践(如DRY原则、最小权限原则、响应式设计等)
-代码安全性:在编写代码时,始终考虑安全性,避免引入漏洞,确保用户输入的安全处理
-性能优化:优化代码的性能,减少资源占用,提升加载速度,确保项目的高效运行
-测试与文档:编写单元测试,确保代码的健壮性,并提供清晰的中文注释和文档。方便后续阅读和维护
##问题解决
-全面阅读相关代码,理解***应用的工作原理
-根据用户的反馈分析问题的原因,提出解决问题的思路
-确保每次代码变更不会破坏现有功能,且尽可能保持最小的改动
##迭代优化
与用户保持密切沟通,根据反读调整功能和设计,确保应用符合用户需求
在不确定需求时,主动询问用户以澄清需求或技术细节
##方法论
-系统2思维:以分析严谨的方式解决问题。将需求分解为更小、可管理的部分,并在实施前仔细考虑每一步
思维树:评估多种可能的解决方案及其后果。使用结构化的方法探索不同的路径。并选择最优的解决方案
-选代改进:在最终确定代码之前,考虑改进、边缘情况和优化。通过潜在增强的迭代,确保最终解决方案是健壮的

235
.claude/CLAUDE.md Normal file
View File

@@ -0,0 +1,235 @@
---
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个综合性的 **RuoYi-Vue 企业管理系统**,并集成了跨境电商 **ERP 功能**。项目主要包含:
* **RuoYi-Vue 核心**:基于 Spring Boot 后端和 Vue.js 前端的企业管理系统
* **ERP 客户端 (erp\_client\_sb)**:跨境电商 ERP 系统JavaFX 桌面应用
* **自定义扩展**客户端监控、数据报表、API 集成功能
## 架构概览
### 后端架构 (Spring Boot)
后端采用模块化 Spring Boot 架构,职责清晰:
```
ruoyi-admin/ # 主应用入口和 Web 控制器
ruoyi-framework/ # 核心框架配置 (安全、Redis、MyBatis)
ruoyi-system/ # 系统领域实体、Mapper、业务逻辑
ruoyi-common/ # 公共工具类、常量、通用功能
ruoyi-quartz/ # 定时任务管理
ruoyi-generator/ # 代码生成工具
```
**主要技术栈:**
* Spring Boot 2.5.15 (Java 17)
* Spring Security 5.7.12 + JWT 认证
* MyBatis + PageHelper 分页插件
* MySQL 数据库 + Redis 缓存与会话管理
* Druid 1.2.23 连接池
* Swagger 3.0.0 API 文档
* Selenium 4.34.0 和 WebMagic 1.0.3 网页自动化
### 前端架构 (Vue.js)
前端是一个基于 Vue 2.6.12 的应用UI 使用 Element UI
```
ruoyi-ui/src/
api/ # API 服务模块
components/ # 可复用的 Vue 组件
layout/ # 应用布局组件
router/ # Vue Router 配置
store/ # Vuex 状态管理
utils/ # 工具函数
views/ # 页面组件,按功能组织
```
**主要技术栈:**
* Vue 2.6.12 + Vue Router 3.4.9
* Element UI 2.15.14
* Vuex 3.6.0
* Axios 网络请求
### ERP 客户端架构 (JavaFX + Spring Boot)
独立桌面应用,支持跨境电商业务:
* Spring Boot 3.5.4 + JavaFX 17.0.1 UI
* SQLite 3.42.0 数据库 + JPA/Hibernate
* Playwright 1.54.0 和 WebMagic 1.0.3 数据采集
* HutoolUtils 5.8.36 工具包
* 七牛云存储集成
* 多平台支持亚马逊、日本乐天、Shopee 等)
## 常用开发命令
### 后端 (Maven)
```bash
# 构建打包
mvn clean package
# 启动 Spring Boot 应用
mvn spring-boot:run
# 从 ruoyi-admin 模块运行
cd ruoyi-admin && mvn spring-boot:run
# 跳过测试打包
mvn clean package -DskipTests
# 运行测试
mvn test
# Windows 运行脚本
ry.bat
```
### 前端 (Vue.js)
```bash
cd ruoyi-ui
# 安装依赖
npm install
# 国内镜像源安装
npm install --registry=https://registry.npmmirror.com
# 启动开发服务 (默认端口 80)
npm run dev
# 生产环境构建
npm run build:prod
# 测试环境构建
npm run build:stage
```
### ERP 客户端 (JavaFX)
```bash
cd erp_client_sb
# 启动应用
mvn spring-boot:run
# 打包可执行 JAR
mvn clean package
# 使用 JavaFX 插件运行
mvn javafx:run
# 运行测试
mvn test
```
## 核心配置说明
### 数据库
* **主库**MySQL (Druid 连接池)
* **Redis**:缓存、会话、分布式锁
* **MyBatis**XML SQL 映射,内置分页
* **ERP 客户端**SQLite 本地存储
### 安全与认证
* **JWT**:无状态认证,可配置过期时间
* **Spring Security**:基于角色的访问控制
* **CORS**:跨域通信
* **XSS 防护**:输入过滤
### API 设计
* **RESTful 风格**:标准 HTTP 动词和状态码
* **客户端监控接口**ERP 客户端状态上报
* **匿名访问**:如 `/monitor/client/api/**`
* **统一分页**PageHelper 插件
* **统一响应格式**AjaxResult 封装
* **跨域配置**CORS 支持
* **Swagger 接口文档**`/swagger-ui/index.html`
## 开发流程
### 新功能开发
1. 后端:在 `ruoyi-system/domain/` 新建实体
2. 数据库:在 `ruoyi-system/mapper/` 增加 Mapper
3. 服务层:实现业务逻辑
4. 控制层:在 `ruoyi-admin/web/controller/` 增加接口
5. 前端:开发 Vue 页面和 API 模块
6. 集成:更新路由和菜单
### 客户端集成功能
* 实时状态跟踪
* 错误上报与日志
* 数据采集统计
* API 请求监控
* 版本分发追踪
### 代码生成器
* 访问 `/tool/gen` 使用
* 自动生成Java 实体、Mapper、Service、Controller
* 自动生成前端 Vue 页面和 API
* 自动生成数据库 SQL
## 注意事项
### 文件上传与静态资源
* 上传路径在 `application.yml` 中配置 (`ruoyi.profile`)
* 支持本地存储和七牛云存储
* 静态资源路径 `/profile/**`
* 前端开发服务器端口80 (可在 `vue.config.js` 中修改)
### 系统监控
* 系统监控接口 `/monitor/server`
* Redis 缓存监控
* Druid 连接池监控
* Quartz 定时任务
* 操作日志审计
### 多平台支持
ERP 客户端支持:
* 亚马逊 (Amazon)
* 日本乐天 (Rakuten)
* Shopee
* 阿里巴巴 1688
* 自定义采集与自动化工具
### 重要配置文件
* **后端主配置**`ruoyi-admin/src/main/resources/application.yml`
* **数据库配置**`application-druid.yml`
* **前端配置**`ruoyi-ui/vue.config.js`
* **Maven 配置**:根目录 `pom.xml`(父项目)和各模块 `pom.xml`
* **ERP 客户端配置**`erp_client_sb/src/main/resources/application.yml`
### 常见问题排查
* **端口冲突**:前端默认端口 80可在 `vue.config.js` 修改
* **跨域问题**:检查 `CorsConfig.java``vue.config.js` 代理配置
* **数据库连接**:检查 `application-druid.yml` 中的数据库连接配置
* **Maven 依赖**:使用阿里云镜像加速依赖下载
---
⚠️ **额外要求**:回答时必须使用中文。
⚠️ **代码规范要求**代码必须遵循CLAUDE.md中的规范代码简洁度和性能优先。
💡 **操作提示**:在每次修改代码前,先向我说明修改的思路和方案,我确认同意后再进行代码更改。
---

213
.claude/settings.local.json Normal file
View File

@@ -0,0 +1,213 @@
{
"permissions": {
"allow": [
"Bash(mvn clean:*)",
"Bash(where mysql)",
"Bash(dir:*)",
"Bash(mvn:*)",
"Bash(npm run dev:*)",
"Bash(xmllint:*)",
"Bash(sed:*)",
"Bash(rmdir:*)",
"Bash(rm:*)",
"mcp__talktofigma__get_selection",
"mcp__talktofigma__join_channel",
"mcp__talktofigma__get_document_info",
"mcp__talktofigma__read_my_design",
"mcp__talktofigma__get_node_info",
"mcp__talktofigma__scan_text_nodes",
"mcp__talktofigma__scan_nodes_by_types",
"mcp__talktofigma__get_nodes_info",
"Bash(mvn compile:*)",
"Bash(mkdir:*)",
"Bash(copy:*)",
"Bash(cp:*)",
"Bash(mvn:*)",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\module-info.java\")",
"Bash(jar tf:*)",
"Bash(./build-simple.bat)",
"Bash(java:*)",
"Bash(./build-custom-installer.bat)",
"Bash(echo $JAVA_HOME)",
"Bash(where java)",
"Bash(./build-installer-fixed.bat)",
"Bash(fix-npm-install.bat)",
"Bash(chmod:*)",
"Bash(where mvn)",
"Bash(./install-nsis.bat)",
"Bash(./test-final.bat)",
"Bash(./build-final-installer.bat)",
"Bash(npm install)",
"Bash(./快速启动-ERP客户端.bat)",
"Bash(npm install:*)",
"Bash(curl:*)",
"Bash(rg:*)",
"Bash(timeout:*)",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js\\table-scroll-fix.js\")",
"mcp__context7-mcp__resolve-library-id",
"WebSearch",
"Bash(findstr:*)",
"Bash(move:*)",
"Bash(jar:*)",
"Bash(cat:*)",
"Bash(taskkill:*)",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\test\\SeleniumBrowserTest.java\")",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\RakutenCacheService.java\")",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\CacheService.java\")",
"Bash(git checkout:*)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views\\monitor\\key/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\java\\com\\ruoyi\\system\\domain/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\resources\\mapper\\system/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-common\\src\\main\\java\\com\\ruoyi\\common\\core\\redis/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\java\\com\\ruoyi\\system\\domain/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\resources\\mapper\\system/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\util/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\utils/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\utils/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
"Read(//c/Users/ZiJIe/Desktop/MongooCrawler-feature-monitor/**)",
"Bash(wmic process where:*)",
"Bash(del:*)",
"Bash(ren:*)",
"Bash(find:*)",
"Bash(netstat:*)",
"Bash(mysql:*)",
"Bash(rd:*)",
"Bash(git clean:*)",
"Bash(tasklist:*)",
"Bash(./fix-electron.bat)",
"Bash(npm run build:*)",
"Bash(node:*)",
"Bash(tsc)",
"Bash(npx tsc:*)",
"Bash(./create-dev-version.bat)",
"Bash(./prepare-backend.bat)",
"Bash(./start-erp.bat)",
"Bash(npm config set:*)",
"Bash(set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/)",
"Bash(npm cache clean:*)",
"Bash(npx vite:*)",
"Bash(./dev-ruoyi-erp.bat)",
"Bash(./start-desktop-app.bat)",
"Bash(cnpm install:*)",
"Bash(cnpm uninstall:*)"
],
"deny": [],
"ask": [],
"additionalDirectories": [
"C:\\c\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue",
"C:\\Users\\ZiJIe\\Desktop\\wox",
"C:\\c\\Users\\ZiJIe"
]
},
"dangerouslySkipPermissions": true
}

24
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="ruoyi-framework" />
<module name="ruoyi-system" />
<module name="ruoyi-common" />
<module name="ruoyi-generator" />
<module name="erp_client_sb" />
<module name="ruoyi-quartz" />
<module name="ruoyi-admin" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="erp_client_sb" options="-parameters" />
</option>
</component>
</project>

20
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/erp_client_sb/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-admin/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-admin/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-framework/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-framework/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-generator/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-generator/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-quartz/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-quartz/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-system/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/ruoyi-system/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

35
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="public" />
<option name="name" value="aliyun nexus" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun-repository" />
<option name="name" value="aliyun repository" />
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss-repository" />
<option name="name" value="jboss repository" />
<option name="url" value="http://repository.jboss.org/nexus/content/groups/public-jboss/" />
</remote-repository>
</component>
</project>

13
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
<option value="$PROJECT_DIR$/erp_client_sb/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK" />
</project>

7
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

111
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="71d19dd6-7472-4ebf-b309-b7afee3f99de" name="更改" comment="1" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="332JslhtSnNRRZRMrLiHaPZ3q2S" />
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RunManager" selected="Spring Boot.RuoYiApplication">
<configuration default="true" type="JetRunConfigurationType">
<module name="RuoYi-Vue" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
<module name="RuoYi-Vue" />
<option name="filePath" />
<method v="2" />
</configuration>
<configuration name="ErpClientSbApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="erp_client_sb" />
<option name="SPRING_BOOT_MAIN_CLASS" value="com.tashow.erp.ErpClientSbApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="RuoYiApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="ruoyi-admin" />
<option name="SPRING_BOOT_MAIN_CLASS" value="com.ruoyi.RuoYiApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.23774.435" />
<option value="bundled-js-predefined-d6986cc7102b-f27c65a3e318-JavaScript-IU-251.23774.435" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="71d19dd6-7472-4ebf-b309-b7afee3f99de" name="更改" comment="" />
<created>1758509443816</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1758509443816</updated>
<workItem from="1758509446082" duration="2966000" />
<workItem from="1758512571699" duration="311000" />
</task>
<task id="LOCAL-00001" summary="1">
<option name="closed" value="true" />
<created>1758511833782</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1758511833782</updated>
</task>
<task id="LOCAL-00002" summary="1">
<option name="closed" value="true" />
<created>1758512348322</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1758512348322</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="1" />
<option name="LAST_COMMIT_MESSAGE" value="1" />
</component>
</project>

211
CLAUDE.md Normal file
View File

@@ -0,0 +1,211 @@
---
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个基于 **RuoYi-Vue 3.9.0** 的企业管理系统,集成了跨境电商 ERP 功能。项目包含:
* **RuoYi-Vue 核心**:基于 Spring Boot 2.5.15 后端和 Vue.js 2.6.12 前端的企业管理平台
* **ERP 客户端 (erp_client_sb)**:独立的跨境电商 ERP 桌面应用 (JavaFX + Spring Boot 3.5.4)
* **客户端监控扩展**实时监控、数据报表、API 集成等自定义功能
## 项目架构
### 主项目模块结构 (Maven 多模块)
```
ruoyi-admin/ # 主应用入口Web 控制器层
ruoyi-framework/ # 核心框架配置 (Spring Security, Redis, MyBatis)
ruoyi-system/ # 系统核心模块 (实体类、Mapper、Service)
ruoyi-common/ # 公共工具类和常量定义
ruoyi-quartz/ # 定时任务管理模块
ruoyi-generator/ # 代码生成器模块
ruoyi-ui/ # Vue.js 前端应用
erp_client_sb/ # 独立的跨境电商 ERP 客户端
```
### 技术栈详情
**后端 (Spring Boot 2.5.15)**
- Java 17 + Maven 3.11.0
- Spring Security 5.7.12 + JWT 0.9.1 认证
- MyBatis + PageHelper 1.4.7 分页
- MySQL + Redis 缓存
- Druid 1.2.23 连接池
- Swagger 3.0.0 API 文档
- FastJSON 2.0.57 JSON 处理
**前端 (Vue.js 2.6.12)**
- Element UI 2.15.14 组件库
- Vue Router 3.4.9 + Vuex 3.6.0
- Axios 0.28.1 HTTP 客户端
- ECharts 5.4.0 图表库
- Webpack 构建 (vue-cli-service 4.4.6)
**ERP 客户端 (JavaFX + Spring Boot 3.5.4)**
- OpenJFX 17.0.1 桌面 UI
- SQLite 3.42.0 本地数据库 + JPA/Hibernate
- WebMagic 1.0.3 + Selenium 4.23.0 网页爬取
- HutoolUtils 5.8.36 工具库
- 七牛云 7.12.1 存储服务
## 常用开发命令
### 后端开发 (Maven)
```bash
# 启动主应用 (推荐)
cd ruoyi-admin && mvn spring-boot:run
# 从根目录启动
mvn spring-boot:run -pl ruoyi-admin
# 打包部署
mvn clean package -DskipTests
# 运行测试
mvn test
# 编译项目
mvn clean compile
```
### 前端开发 (npm)
```bash
cd ruoyi-ui
# 安装依赖 (建议使用国内镜像)
npm install --registry=https://registry.npmmirror.com
# 启动开发服务器 (端口 80)
npm run dev
# 生产环境构建
npm run build:prod
# 测试环境构建
npm run build:stage
```
### ERP 客户端开发
```bash
cd erp_client_sb
# 启动 JavaFX 应用
mvn spring-boot:run
# 或者
mvn javafx:run
# 打包可执行 JAR
mvn clean package
```
## 核心配置与架构要点
### Maven 依赖管理
- 使用阿里云 Maven 镜像 (maven.aliyun.com)
- 父 POM 统一管理版本号和依赖
- 安全版本覆盖Tomcat 9.0.106, Logback 1.2.13, Spring Framework 5.3.39
### 前端开发配置
- **开发服务器**:端口 80支持热重载
- **代理配置**`vue.config.js` 中配置后端 API 代理 (`http://8.138.23.49:8080`)
- **构建优化**Gzip 压缩、代码分割、Element UI 单独打包
### 数据库与缓存
- **MySQL**:主数据库,通过 Druid 连接池管理
- **Redis**:会话存储、缓存、分布式锁
- **SQLite**ERP 客户端本地数据存储
### 安全与认证
- **JWT 无状态认证**:支持多终端
- **RBAC 权限模型**:角色-菜单-按钮权限
- **跨域配置**:支持前后端分离部署
- **XSS 防护**:输入过滤和输出编码
## 开发工作流
### 新功能开发标准流程
1. **后端开发**
-`ruoyi-system/src/main/java/com/ruoyi/system/domain/` 创建实体类
-`ruoyi-system/src/main/java/com/ruoyi/system/mapper/` 创建 Mapper 接口
-`ruoyi-system/src/main/resources/mapper/system/` 创建 MyBatis XML
-`ruoyi-system/src/main/java/com/ruoyi/system/service/` 实现业务逻辑
-`ruoyi-admin/src/main/java/com/ruoyi/web/controller/` 创建 REST 控制器
2. **前端开发**
-`ruoyi-ui/src/api/` 创建 API 服务模块
-`ruoyi-ui/src/views/` 创建页面组件
- 更新路由配置和菜单权限
3. **代码生成器**
- 访问 `/tool/gen` 快速生成 CRUD 代码
- 支持 Java、Vue、SQL 代码自动生成
### 客户端集成开发
ERP 客户端提供的核心功能:
- 多平台数据采集 (Amazon, Rakuten, Shopee, 1688)
- 实时状态监控与错误上报
- 与主系统的 API 集成 (`/monitor/client/api/**`)
## 重要配置文件
### 后端配置
- **主配置**`ruoyi-admin/src/main/resources/application.yml`
- **数据源配置**`ruoyi-admin/src/main/resources/application-druid.yml`
- **Maven 配置**:根目录 `pom.xml` (父 POM)
### 前端配置
- **构建配置**`ruoyi-ui/vue.config.js`
- **包管理**`ruoyi-ui/package.json`
### ERP 客户端配置
- **应用配置**`erp_client_sb/src/main/resources/application.yml`
- **Maven 配置**`erp_client_sb/pom.xml`
## 系统监控与工具
### 内置监控功能
- 系统性能监控:`/monitor/server`
- Redis 缓存监控:缓存信息查询和命令统计
- Druid 连接池监控SQL 性能分析
- 在线用户监控:当前活跃用户状态
- 操作日志:系统操作记录和异常日志
### API 文档
- Swagger UI`/swagger-ui/index.html`
- 自动生成的接口文档
## 部署和环境
### 开发环境要求
- **Java**JDK 17
- **Node.js**>= 8.9
- **npm**>= 3.0.0
- **MySQL**5.7+ 或 8.0+
- **Redis**6.0+
### 常见问题排查
- **端口冲突**:前端默认 80 端口,可在 `vue.config.js` 修改
- **跨域问题**:检查 `vue.config.js` 代理配置和后端 CORS 设置
- **Maven 依赖**:已配置阿里云镜像加速
- **JavaFX 运行**:确保 JDK 包含 JavaFX 模块或单独安装 OpenJFX
---
⚠️ **额外要求**:回答时必须使用中文。
💡 **操作提示**:在每次修改代码前,我会先向您说明修改的思路和方案,请您确认同意后再进行代码更改。
---

95
README.md Normal file
View File

@@ -0,0 +1,95 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.0</h1>
<h4 align="center">基于SpringBoot+Vue前后端分离的Java快速开发框架</h4>
<p align="center">
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.0-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
</p>
## 平台简介
若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 前端采用Vue、Element UI。
* 后端采用Spring Boot、Spring Security、Redis & Jwt。
* 权限认证使用Jwt支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。
* 高效率开发,使用代码生成器可以一键生成前后端代码。
* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3),保持同步更新。
* 提供了单应用版本[RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast)Oracle版本[RuoYi-Vue-Oracle](https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle),保持同步更新。
* 不分离版本,请移步[RuoYi](https://gitee.com/y_project/RuoYi),微服务版本,请移步[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud)
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
13. 代码生成前后端代码的生成java、html、xml、sql支持CRUD下载 。
14. 系统接口根据业务代码自动生成相关的api接口文档。
15. 服务监控监视当前系统CPU、内存、磁盘、堆栈等相关信息。
16. 缓存监控:对系统的缓存信息查询,命令统计等。
17. 在线构建器拖动表单元素生成相应的HTML代码。
18. 连接池监视监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈。
## 在线体验
- admin/admin123
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。
演示地址http://vue.ruoyi.vip
文档地址http://doc.ruoyi.vip
## 演示图
<table>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-8074972883b5ba0622e13246738ebba237a.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-9f88719cdfca9af2e58b352a20e23d43b12.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-39bf2584ec3a529b0d5a3b70d15c9b37646.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-936ec82d1f4872e1bc980927654b6007307.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-b2d62ceb95d2dd9b3fbe157bb70d26001e9.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-d67451d308b7a79ad6819723396f7c3d77a.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-8370a0d02977eebf6dbf854c8450293c937.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-49003ed83f60f633e7153609a53a2b644f7.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-d4fe726319ece268d4746602c39cffc0621.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
</tr>
</table>
## 若依前后端分离交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) 点击按钮入群。

1
data/device.id Normal file
View File

@@ -0,0 +1 @@
DEVICE-C705AA3904F84D998D03B5CD83EEBBD7

BIN
data/erp-cache.db Normal file

Binary file not shown.

BIN
data/erp-cache.db-shm Normal file

Binary file not shown.

BIN
data/erp-cache.db-wal Normal file

Binary file not shown.

28
data/jwt_rsa_private.pem Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCMgaMZlZOiAK7e
FWjhWuyIg+8XBIrF9/kZYGtX+3tn53T+sCPBjAIukouyP41H5g6IZDeTP1WXsNuQ
kVIvbZwRJKTSO7EiqE1T3V9jPzTXOXvq0SPnjaMhmUKxkmgRSs9DBZUqrElZ5mzw
1ua/9ISlwCLqFRrVL5Nzc9+TO8IIRjjPBPYssYCDpz4NpdQb4OQYhaTRrj1++wo4
N+LuS+wKb0Ps8SH1+udYaUWR6NteR6hhj0+0AVK0cyqsbHYQE4z+gAJr+Aicd1ZX
pON8jsFiSNdTgGIVKCb8haVkgkXwe1inBwhyVcWk8xaz2aKa+TsNI03WfBi8DY39
+WeisqnrAgMBAAECggEABJA3TF7rxxCvnT3jxKHv2bUzQDOhEDHwEK9tfROJXAQL
7DOrTZ9u+LVAvT7MJ2Ak67AZj/o4HO+dCfJ2UV0FexcOFVfj9mSx8j3X2cDVRgIz
cJpvSJd0i2RPYrYHFDyyQ5J8WED1NurBcgcAwo49+qYlXCXoU7EyYEcMpVsE/8C/
xvW0GShTzXOpO5emQOkPixxNW8i6VHte23igUULn29eyePB+pvRdEuuKC+bEZ9cJ
ZXJ0p/yS2sNUxvBAmdVZTTs88E3gu/ARRJgyHE//Ax+fHmlSpPGeJlEPivntu0So
vrJan9uIA3mS7eE1i7M+JoZjIgwpHKfG07AipRmklQKBgQDEQAGgpMo3AtRMmYFi
PP3Sb5p3cETekWXJBijlhE69XBZg/VBSpNrtmD4iXuYbBb3d140nIvESowpBN3gn
uY6nNqJWSzhKzQhU8ienJS8PZPeeRE2mMI8sRxl+VnhMraa/E8k/hzqP0RCo+cfC
uu1be7AJ0+W6WB0d9g6zSj99HQKBgQC3SOV/4i1KXybuaphJJrZF5cuMoD5cE5Ck
vXTTc9MSkUtwLf5PiJGRzRWNd5amS0FMb3IuC8YfEqZ8k+vDDVbgigqDeIKEpJSQ
3K8A/F/094cyMyqrQ2aiUPg6Da57QH+Q0Ab2S7n8RstZC6h28sYxgG1acuL2jqY7
S7HlVAZ8pwKBgQC3UqMykT1kjfwLYgn+3sKsZRyCHhn3XxMZ6esiG6oCMZemGnuB
+AWalPDV4phI/eAS71woBvfzVOIrccmIMkoT4XFb8wAuv8DcuShZdt6zHrpA2cU/
TXUxA2nJHrVZy41MSQthkM0fs0hA0LPOMBexsaUMSSj8HXt1lXi9+sm78QKBgAsE
0udRTa++8LQ8rFMZhLPHEOmvaJBYjMWarj9YI0Rmf8aKvVNCvp2pWrZajjAJLi/O
M2sZQhv0HxY2PmJHlwWAxwkIYbBfxJ7A5bSFd69egj4+XT5WmwD/JS04TVkTk5e9
Ke38t323M9pynPoptkibk/dwGL0B7nR6JIPI/WrZAoGBALYitr4UK/H8I/XvYaUJ
q0A93fIa9ACVHshZEAwY7o1PIZu9bLScGilkbkvdATwgkfrBZXFCDIAFu2Ta/yMF
NEFXG1szFg0aDzEN0pm8o2dndQ9dKTPswytvR5DphgCmxhuJDm9Qwwjc1UQuWePp
Q3mDFK0eVcWWdA03UbY2TEeK
-----END PRIVATE KEY-----

9
data/jwt_rsa_public.pem Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjIGjGZWTogCu3hVo4Vrs
iIPvFwSKxff5GWBrV/t7Z+d0/rAjwYwCLpKLsj+NR+YOiGQ3kz9Vl7DbkJFSL22c
ESSk0juxIqhNU91fYz801zl76tEj542jIZlCsZJoEUrPQwWVKqxJWeZs8Nbmv/SE
pcAi6hUa1S+Tc3PfkzvCCEY4zwT2LLGAg6c+DaXUG+DkGIWk0a49fvsKODfi7kvs
Cm9D7PEh9frnWGlFkejbXkeoYY9PtAFStHMqrGx2EBOM/oACa/gInHdWV6TjfI7B
YkjXU4BiFSgm/IWlZIJF8HtYpwcIclXFpPMWs9mimvk7DSNN1nwYvA2N/flnorKp
6wIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Deluze
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,77 @@
<div align="center">
# Electron Vue Template
<img width="794" alt="image" src="https://user-images.githubusercontent.com/32544586/222748627-ee10c9a6-70d2-4e21-b23f-001dd8ec7238.png">
A simple starter template for a **Vue3** + **Electron** TypeScript based application, including **ViteJS** and **Electron Builder**.
</div>
## About
This template utilizes [ViteJS](https://vitejs.dev) for building and serving your (Vue powered) front-end process, it provides Hot Reloads (HMR) to make development fast and easy ⚡
Building the Electron (main) process is done with [Electron Builder](https://www.electron.build/), which makes your application easily distributable and supports cross-platform compilation 😎
## Getting started
Click the green **Use this template** button on top of the repository, and clone your own newly created repository.
**Or..**
Clone this repository: `git clone git@github.com:Deluze/electron-vue-template.git`
### Install dependencies ⏬
```bash
npm install
```
### Start developing ⚒️
```bash
npm run dev
```
## Additional Commands
```bash
npm run dev # starts application with hot reload
npm run build # builds application, distributable files can be found in "dist" folder
# OR
npm run build:win # uses windows as build target
npm run build:mac # uses mac as build target
npm run build:linux # uses linux as build target
```
Optional configuration options can be found in the [Electron Builder CLI docs](https://www.electron.build/cli.html).
## Project Structure
```bash
- scripts/ # all the scripts used to build or serve your application, change as you like.
- src/
- main/ # Main thread (Electron application source)
- renderer/ # Renderer thread (VueJS application source)
```
## Using static files
If you have any files that you want to copy over to the app directory after installation, you will need to add those files in your `src/main/static` directory.
Files in said directory are only accessible to the `main` process, similar to `src/renderer/assets` only being accessible to the `renderer` process. Besides that, the concept is the same as to what you're used to in your other front-end projects.
#### Referencing static files from your main process
```ts
/* Assumes src/main/static/myFile.txt exists */
import {app} from 'electron';
import {join} from 'path';
import {readFileSync} from 'fs';
const path = join(app.getAppPath(), 'static', 'myFile.txt');
const buffer = readFileSync(path);
```

View File

@@ -0,0 +1,39 @@
{
"appId": "com.electron.app",
"directories": {
"output": "dist"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"shortcutName": "Electron App"
},
"win": {
"target": "nsis"
},
"linux": {
"target": ["snap"]
},
"files": [
{
"from": "build/main",
"to": "main",
"filter": ["**/*"]
},
{
"from": "build/renderer",
"to": "renderer",
"filter": ["**/*"]
},
{
"from": "src/main/static",
"to": "static",
"filter": ["**/*"]
},
"!build",
"!dist",
"!scripts"
]
}

5847
electron-vue-template/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "electron-vue-template",
"version": "0.1.0",
"description": "A minimal Electron + Vue application",
"main": "main/main.js",
"scripts": {
"dev": "node scripts/dev-server.js",
"build": "node scripts/build.js && electron-builder",
"build:win": "node scripts/build.js && electron-builder --win",
"build:mac": "node scripts/build.js && electron-builder --mac",
"build:linux": "node scripts/build.js && electron-builder --linux"
},
"repository": "https://github.com/deluze/electron-vue-template",
"author": {
"name": "Deluze",
"url": "https://github.com/Deluze"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.1",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"electron": "^32.1.2",
"electron-builder": "^25.1.6",
"typescript": "^5.2.2",
"vite": "^4.5.0"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"element-plus": "^2.11.3",
"exceljs": "^4.4.0",
"vue": "^3.3.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,33 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正在启动...</title>
<style>
html, body { height: 100%; margin: 0; }
body {
background: #fff; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
background-image: url('image/splash_screen.png');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.box { position: fixed; left: 0; right: 0; bottom: 28px; padding: 0 0; }
.progress { position: relative; width: 100vw; height: 6px; background: rgba(0,0,0,0.08); }
.bar { position: absolute; left: 0; top: 0; height: 100%; width: 20vw; min-width: 120px; background: linear-gradient(90deg, #67C23A, #409EFF); animation: slide 1s ease-in-out infinite alternate; }
@keyframes slide { 0% { left: 0; } 100% { left: calc(100vw - 20vw); } }
</style>
<link rel="icon" href="icon/icon.png">
<link rel="apple-touch-icon" href="icon/icon.png">
<meta name="theme-color" content="#ffffff">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';">
</head>
<body>
<div class="box">
<div class="progress"><div class="bar"></div></div>
</div>
</body>
</html>

View File

@@ -0,0 +1,32 @@
const Path = require('path');
const Chalk = require('chalk');
const FileSystem = require('fs');
const Vite = require('vite');
const compileTs = require('./private/tsc');
function buildRenderer() {
return Vite.build({
configFile: Path.join(__dirname, '..', 'vite.config.js'),
base: './',
mode: 'production'
});
}
function buildMain() {
const mainPath = Path.join(__dirname, '..', 'src', 'main');
return compileTs(mainPath);
}
FileSystem.rmSync(Path.join(__dirname, '..', 'build'), {
recursive: true,
force: true,
})
console.log(Chalk.blueBright('Transpiling renderer & main...'));
Promise.allSettled([
buildRenderer(),
buildMain(),
]).then(() => {
console.log(Chalk.greenBright('Renderer & main successfully transpiled! (ready to be built with electron-builder)'));
});

View File

@@ -0,0 +1,121 @@
process.env.NODE_ENV = 'development';
const Vite = require('vite');
const ChildProcess = require('child_process');
const Path = require('path');
const Chalk = require('chalk');
const Chokidar = require('chokidar');
const Electron = require('electron');
const compileTs = require('./private/tsc');
const FileSystem = require('fs');
const { EOL } = require('os');
let viteServer = null;
let electronProcess = null;
let electronProcessLocker = false;
let rendererPort = 0;
async function startRenderer() {
viteServer = await Vite.createServer({
configFile: Path.join(__dirname, '..', 'vite.config.js'),
mode: 'development',
});
return viteServer.listen();
}
async function startElectron() {
if (electronProcess) { // single instance lock
return;
}
try {
await compileTs(Path.join(__dirname, '..', 'src', 'main'));
} catch {
console.log(Chalk.redBright('Could not start Electron because of the above typescript error(s).'));
electronProcessLocker = false;
return;
}
const args = [
Path.join(__dirname, '..', 'build', 'main', 'main.js'),
rendererPort,
];
electronProcess = ChildProcess.spawn(Electron, args);
electronProcessLocker = false;
electronProcess.stdout.on('data', data => {
if (data == EOL) {
return;
}
process.stdout.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString()))
});
electronProcess.stderr.on('data', data =>
process.stderr.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString()))
);
electronProcess.on('exit', () => stop());
}
function restartElectron() {
if (electronProcess) {
electronProcess.removeAllListeners('exit');
electronProcess.kill();
electronProcess = null;
}
if (!electronProcessLocker) {
electronProcessLocker = true;
startElectron();
}
}
function copyStaticFiles() {
copy('static');
}
/*
The working dir of Electron is build/main instead of src/main because of TS.
tsc does not copy static files, so copy them over manually for dev server.
*/
function copy(path) {
FileSystem.cpSync(
Path.join(__dirname, '..', 'src', 'main', path),
Path.join(__dirname, '..', 'build', 'main', path),
{ recursive: true }
);
}
function stop() {
viteServer.close();
process.exit();
}
async function start() {
console.log(`${Chalk.greenBright('=======================================')}`);
console.log(`${Chalk.greenBright('Starting Electron + Vite Dev Server...')}`);
console.log(`${Chalk.greenBright('=======================================')}`);
const devServer = await startRenderer();
rendererPort = devServer.config.server.port;
copyStaticFiles();
startElectron();
const path = Path.join(__dirname, '..', 'src', 'main');
Chokidar.watch(path, {
cwd: path,
}).on('change', (path) => {
console.log(Chalk.blueBright(`[electron] `) + `Change in ${path}. reloading... 🚀`);
if (path.startsWith(Path.join('static', '/'))) {
copy(path);
}
restartElectron();
});
}
start();

View File

@@ -0,0 +1,24 @@
const ChildProcess = require('child_process');
const Chalk = require('chalk');
function compile(directory) {
return new Promise((resolve, reject) => {
const tscProcess = ChildProcess.exec('tsc', {
cwd: directory,
});
tscProcess.stdout.on('data', data =>
process.stdout.write(Chalk.yellowBright(`[tsc] `) + Chalk.white(data.toString()))
);
tscProcess.on('exit', exitCode => {
if (exitCode > 0) {
reject(exitCode);
} else {
resolve();
}
});
});
}
module.exports = compile;

View File

@@ -0,0 +1,199 @@
// 主进程:创建窗口、启动后端 JAR、隐藏菜单栏
import {app, BrowserWindow, ipcMain, session, Menu, screen} from 'electron';
import { Socket } from 'net';
import { existsSync } from 'fs';
import {join, dirname} from 'path';
import {spawn, ChildProcessWithoutNullStreams} from 'child_process';
// 保存后端进程与窗口引用,便于退出时清理
let springProcess: ChildProcessWithoutNullStreams | null = null;
let mainWindow: BrowserWindow | null = null;
let splashWindow: BrowserWindow | null = null;
let appOpened = false;
function openAppIfNotOpened() {
if (appOpened) return;
appOpened = true;
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
if (splashWindow) { splashWindow.close(); splashWindow = null; }
}
// 启动后端 Spring Boot使用你提供的绝对路径
function startSpringBoot() {
const jarPath = 'C:/Users/ZiJIe/Desktop/wox/RuoYi-Vue/ruoyi-admin/target/ruoyi-admin.jar';
springProcess = spawn('java', ['-jar', jarPath], {
cwd: dirname(jarPath),
detached: false
});
// 打印后端日志,监听启动成功标志
springProcess.stdout.on('data', (data) => {
console.log(`SpringBoot: ${data}`);
// 检测到启动成功日志立即进入主界面
if (data.toString().includes('Started RuoYiApplication')) {
openAppIfNotOpened();
}
});
// 打印后端错误,检测启动失败
springProcess.stderr.on('data', (data) => {
console.error(`SpringBoot ERROR: ${data}`);
const errorStr = data.toString();
// 检测到关键错误信息,直接退出
if (errorStr.includes('APPLICATION FAILED TO START') ||
errorStr.includes('Port') && errorStr.includes('already in use') ||
errorStr.includes('Unable to start embedded Tomcat')) {
console.error('后端启动失败,程序即将退出');
app.quit();
}
});
// 后端退出时,前端同步退出
springProcess.on('close', (code) => {
console.log(`SpringBoot exited with code ${code}`);
if (mainWindow) {
mainWindow.close();
} else {
app.quit();
}
});
}
// 关闭后端进程Windows 使用 taskkill 结束整个进程树)
function stopSpringBoot() {
if (!springProcess) return;
try {
if (process.platform === 'win32') {
// Force kill the whole process tree on Windows
try {
const pid = springProcess.pid;
if (pid !== undefined && pid !== null) {
spawn('taskkill', ['/pid', String(pid), '/f', '/t']);
} else {
springProcess.kill();
}
} catch (e) {
// Fallback
springProcess.kill();
}
} else {
springProcess.kill('SIGTERM');
}
} catch (e) {
console.error('Failed to stop Spring Boot process:', e);
} finally {
springProcess = null;
}
}
// 创建主窗口(预创建但隐藏)
function createWindow () {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
}
});
// 彻底隐藏原生菜单栏
try {
Menu.setApplicationMenu(null);
mainWindow.setMenuBarVisibility(false);
if (typeof (mainWindow as any).setMenu === 'function') {
(mainWindow as any).setMenu(null);
}
} catch {}
const rendererPort = process.argv[2];
mainWindow.loadURL(`http://localhost:${rendererPort}`);
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
// 预创建主窗口(隐藏)
createWindow();
// 显示启动页
const { width: sw, height: sh } = screen.getPrimaryDisplay().workAreaSize;
const splashW = Math.min(Math.floor(sw * 0.8), 1800);
const splashH = Math.min(Math.floor(sh * 0.8), 1200);
splashWindow = new BrowserWindow({
width: splashW,
height: splashH,
frame: false,
transparent: false,
resizable: false,
alwaysOnTop: true,
show: true,
center: true,
});
const candidateSplashPaths = [
join(__dirname, '../../public', 'splash.html'),
];
const foundSplash = candidateSplashPaths.find(p => existsSync(p));
if (foundSplash) {
splashWindow.loadFile(foundSplash);
}
// 注释掉后端启动,便于快速调试前端
// startSpringBoot();
// 快速调试模式 - 直接打开主窗口
setTimeout(() => {
openAppIfNotOpened();
}, 1000);
// 注释掉超时机制
/*
setTimeout(() => {
if (!appOpened) {
console.error('后端启动超时,程序即将退出');
app.quit();
}
}, 30000);
*/
// 保守 CSP仅允许自身脚本避免引入不必要的外部脚本
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['script-src \'self\'']
}
})
})
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', function () {
stopSpringBoot();
if (process.platform !== 'darwin') app.quit()
});
app.on('before-quit', () => {
stopSpringBoot();
});
ipcMain.on('message', (event, message) => {
console.log(message);
})

View File

@@ -0,0 +1,5 @@
import {contextBridge, ipcRenderer} from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
sendMessage: (message: string) => ipcRenderer.send('message', message)
})

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "../../build/main",
"allowJs": true,
"noImplicitAny": false,
},
"exclude": ["static"]
}

View File

@@ -0,0 +1,535 @@
<script setup lang="ts">
import { onMounted, ref, computed, defineAsyncComponent } from 'vue'
import { ElConfigProvider, ElMessage, ElMessageBox } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
// 图标已移至对应组件
import 'element-plus/dist/index.css'
import { authApi } from './api/auth'
import { deviceApi, type DeviceItem, type DeviceQuota } from './api/device'
import ZebraDashboard from './components/zebra/ZebraDashboard.vue'
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
const RakutenDashboard = defineAsyncComponent(() => import('./components/rakuten/RakutenDashboard.vue'))
const AmazonDashboard = defineAsyncComponent(() => import('./components/amazon/AmazonDashboard.vue'))
// 导航历史栈
const navigationHistory = ref<string[]>(['rakuten'])
const currentHistoryIndex = ref(0)
// 应用状态
const activeMenu = ref('rakuten')
const isAuthenticated = ref(false)
const showAuthDialog = ref(false)
const showRegDialog = ref(false)
const zhCnLocale = zhCn
const currentUsername = ref('')
const showDeviceDialog = ref(false)
const deviceLoading = ref(false)
const devices = ref<DeviceItem[]>([])
const deviceQuota = ref<DeviceQuota>({ limit: 0, used: 0 })
const userPermissions = ref<string>('')
// 菜单配置 - 复刻ERP客户端格式
const menuConfig = [
{ key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R' },
{ key: 'amazon', name: 'Amazon', index: 'amazon', icon: 'A' },
{ key: 'zebra', name: 'Zebra', index: 'zebra', icon: 'Z' },
{ key: 'shopee', name: 'Shopee', index: 'shopee', icon: 'S' },
]
// 权限检查 - 复刻ERP客户端逻辑
function hasPermission(module: string) {
// 默认显示的基础菜单(未登录时也显示)
const defaultModules = ['rakuten', 'amazon', 'zebra']
if (!isAuthenticated.value) {
return defaultModules.includes(module)
}
const permissions = userPermissions.value
if (!permissions) {
return defaultModules.includes(module) // 没有权限信息时显示默认菜单
}
// 简化权限检查:直接检查模块名是否在权限字符串中
return permissions.includes(module)
}
const visibleMenus = computed(() => menuConfig.filter(item => hasPermission(item.key)))
const canGoBack = computed(() => currentHistoryIndex.value > 0)
const canGoForward = computed(() => currentHistoryIndex.value < navigationHistory.value.length - 1)
function showContent() {
const loading = document.getElementById('loading')
if (loading) {
loading.style.opacity = '0'
setTimeout(() => { loading.style.display = 'none' }, 100)
}
const app = document.getElementById('app-root')
if (app) app.style.opacity = '1'
}
function addToHistory(menu: string) {
if (navigationHistory.value[currentHistoryIndex.value] !== menu) {
navigationHistory.value = navigationHistory.value.slice(0, currentHistoryIndex.value + 1)
navigationHistory.value.push(menu)
currentHistoryIndex.value = navigationHistory.value.length - 1
}
}
function goBack() {
if (canGoBack.value) {
currentHistoryIndex.value--
activeMenu.value = navigationHistory.value[currentHistoryIndex.value]
}
}
function goForward() {
if (canGoForward.value) {
currentHistoryIndex.value++
activeMenu.value = navigationHistory.value[currentHistoryIndex.value]
}
}
function reloadPage() {
window.location.reload()
}
function handleMenuSelect(key: string) {
// 检查是否需要认证
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
if (!isAuthenticated.value && authRequiredMenus.includes(key)) {
showAuthDialog.value = true
return
}
activeMenu.value = key
addToHistory(key)
}
async function handleLoginSuccess(data: { token: string; user: any }) {
isAuthenticated.value = true
showAuthDialog.value = false
try {
currentUsername.value = data?.user?.username || currentUsername.value
userPermissions.value = data?.permissions || data?.user?.permissions || ''
} catch {}
// 登录成功后自动注册设备 - 简化版
try {
const username = data?.user?.username || currentUsername.value
if (username) {
await deviceApi.register({ username })
}
} catch (e) {
// 设备注册失败不影响登录流程,静默处理
console.warn('设备注册失败:', e)
}
}
async function handleUserClick() {
if (!isAuthenticated.value) {
showAuthDialog.value = true
return
}
try {
await ElMessageBox.confirm('确认退出登录?', '提示', { type: 'warning', confirmButtonText: '退出', cancelButtonText: '取消' })
const token = localStorage.getItem('token') || ''
try { await authApi.logout(token) } catch {}
try { localStorage.removeItem('token') } catch {}
isAuthenticated.value = false
currentUsername.value = ''
userPermissions.value = ''
showAuthDialog.value = true
showDeviceDialog.value = false
ElMessage.success('已退出登录')
} catch {}
}
function handleLoginCancel() {
showAuthDialog.value = false
}
function showRegisterDialog() {
showAuthDialog.value = false
showRegDialog.value = true
}
function handleRegisterSuccess() {
showRegDialog.value = false
showAuthDialog.value = true
}
function backToLogin() {
showRegDialog.value = false
showAuthDialog.value = true
}
// 检查认证状态 - 复刻ERP客户端逻辑
async function checkAuth() {
const token = localStorage.getItem('token')
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
if (token) {
try {
const response = await authApi.verifyToken(token)
if (response.success) {
isAuthenticated.value = true
if (!currentUsername.value) {
const u = getUsernameFromToken(token)
if (u) currentUsername.value = u
}
userPermissions.value = response.permissions || ''
return
}
} catch {
localStorage.removeItem('token')
}
}
// 检查是否需要显示登录弹框
if (!isAuthenticated.value && authRequiredMenus.includes(activeMenu.value)) {
showAuthDialog.value = true
}
}
function getClientIdFromToken(token?: string) {
try {
const t = token || localStorage.getItem('token') || ''
const payload = JSON.parse(atob(t.split('.')[1] || ''))
return payload.clientId || ''
} catch { return '' }
}
function getUsernameFromToken(token?: string) {
try {
const t = token || localStorage.getItem('token') || ''
const payload = JSON.parse(atob(t.split('.')[1] || ''))
return payload.username || ''
} catch { return '' }
}
async function openDeviceManager() {
if (!isAuthenticated.value) {
showAuthDialog.value = true
return
}
showDeviceDialog.value = true
await fetchDeviceData()
}
async function fetchDeviceData() {
const username = (currentUsername.value || getUsernameFromToken()).trim()
if (!username) {
ElMessage.warning('未获取到用户名,请重新登录')
return
}
try {
deviceLoading.value = true
const [quota, list] = await Promise.all([
deviceApi.getQuota(username),
deviceApi.list(username),
])
deviceQuota.value = quota || { limit: 0, used: 0 }
const clientId = getClientIdFromToken()
devices.value = (list || []).map(d => ({ ...d, isCurrent: d.deviceId === clientId })) as any
} catch (e: any) {
ElMessage.error(e?.message || '获取设备列表失败')
} finally {
deviceLoading.value = false
}
}
async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
try {
await ElMessageBox.confirm('确定要移除该设备吗?', '你确定要移除设备吗?', { confirmButtonText: '确定移除', cancelButtonText: '取消', type: 'warning' })
await deviceApi.remove({ deviceId: row.deviceId })
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
if (row.isCurrent) {
// 当前设备被移除,清理登录状态
isAuthenticated.value = false
showAuthDialog.value = true
try { localStorage.removeItem('token') } catch {}
}
ElMessage.success('已移除设备')
} catch (e) {
/* 用户取消或失败 */
}
}
onMounted(async () => {
showContent()
await checkAuth()
})
</script>
<template>
<el-config-provider :locale="zhCnLocale">
<div id="app-root" class="root">
<div class="loading-container" id="loading">
<div class="loading-spinner"></div>
</div>
<div class="erp-container">
<div class="sidebar">
<div class="user-avatar">
<img src="/icon/icon.png" alt="logo" />
</div>
<div class="menu-group-title">电商平台</div>
<ul class="menu">
<li
v-for="item in visibleMenus"
:key="item.key"
class="menu-item"
:class="{ active: activeMenu === item.key }"
@click="handleMenuSelect(item.key)"
>
<span class="menu-text"><span class="menu-icon" :data-k="item.key">{{ item.icon }}</span>{{ item.name }}</span>
</li>
</ul>
</div>
<div class="main-content">
<NavigationBar
:can-go-back="canGoBack"
:can-go-forward="canGoForward"
:active-menu="activeMenu"
@go-back="goBack"
@go-forward="goForward"
@reload="reloadPage"
@user-click="handleUserClick"
@open-device="openDeviceManager" />
<div class="content-body">
<div
class="dashboard-home"
v-if="!isAuthenticated && (activeMenu === 'rakuten' || activeMenu === 'amazon' || activeMenu === 'zebra')">
<div class="icon-container">
<img src="/image/111.png" alt="ERP Logo" class="main-icon" />
</div>
</div>
<ZebraDashboard v-if="activeMenu === 'zebra'" />
<RakutenDashboard v-else-if="activeMenu === 'rakuten'" />
<AmazonDashboard v-else-if="activeMenu === 'amazon'" />
<div v-else class="placeholder">
<div class="placeholder-card">
<div class="placeholder-title">{{ activeMenu.toUpperCase() }} 面板</div>
<div class="placeholder-desc">功能开发中...</div>
</div>
</div>
</div>
<!-- 认证组件 -->
<LoginDialog
v-model="showAuthDialog"
@login-success="handleLoginSuccess"
@show-register="showRegisterDialog" />
<RegisterDialog
v-model="showRegDialog"
@register-success="handleRegisterSuccess"
@back-to-login="backToLogin" />
<!-- 设备管理弹框 -->
<el-dialog
:title="`设备管理 (${deviceQuota.used || 0}/${deviceQuota.limit || 0})`"
v-model="showDeviceDialog"
width="560px"
:close-on-click-modal="false">
<div style="margin-bottom: 10px; color:#909399;">当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备</div>
<el-table :data="devices" size="small" :loading="deviceLoading" style="width:100%" stripe>
<el-table-column label="设备名" min-width="180">
<template #default="scope">
<span>{{ scope.row.name || scope.row.deviceId }}</span>
<el-tag v-if="scope.row.isCurrent" size="small" type="success" style="margin-left:6px;">本机</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="scope">
<el-tag :type="scope.row.status==='online' ? 'success' : 'info'" size="small">{{ scope.row.status==='online' ? '已登录' : '已登出' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="最近" min-width="130">
<template #default="scope">
<span>{{ scope.row.lastActiveAt || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button type="text" size="small" style="color:#F56C6C" @click="confirmRemoveDevice(scope.row)">移除设备</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="showDeviceDialog=false">关闭</el-button>
</template>
</el-dialog>
</div>
</div>
</div>
</el-config-provider>
</template>
<style scoped>
.root {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
background-color: #f5f5f5;
opacity: 0;
transition: opacity 0.1s ease;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100%;
position: fixed;
top: 0;
left: 0;
background-color: #f5f5f5;
z-index: 9999;
transition: opacity 0.1s ease;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #e6e6e6;
border-top: 5px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.erp-container {
display: flex;
height: 100vh;
}
.sidebar {
width: 220px;
min-width: 220px;
flex-shrink: 0;
background: #ffffff;
border-right: 1px solid #e8eaec;
padding: 16px 12px;
box-sizing: border-box;
}
.platform-icons { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.picon { width: 28px; height: 28px; object-fit: contain; }
.user-avatar {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 0;
border-bottom: 1px solid #e8eaec;
margin: 0 0 12px 0;
}
.user-avatar img {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: contain;
background: #ffffff;
}
.menu-group-title {
font-size: 12px;
color: #909399;
margin: 8px 6px 10px;
text-align: left; /* “电商平台”四个字靠左 */
}
.menu {
list-style: none;
padding: 0;
margin: 0;
}
.menu-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
color: #333333;
margin-bottom: 4px;
}
.menu-item:hover {
background: #f5f7fa;
}
.menu-item.active {
background: #ecf5ff !important;
color: #409EFF !important;
}
.menu-text {
font-size: 14px;
}
.menu-text { display: inline-flex; align-items: center; gap: 6px; }
.menu-icon { display: inline-flex; width: 18px; height: 18px; border-radius: 4px; align-items: center; justify-content: center; font-size: 12px; color: #fff; }
.menu-icon[data-k="rakuten"] { background: #BF0000; }
.menu-icon[data-k="amazon"] { background: #FF9900; color: #1A1A1A; }
.menu-icon[data-k="zebra"] { background: #34495e; }
.menu-icon[data-k="shopee"] { background: #EE4D2D; }
.main-content {
flex: 1;
min-width: 0;
position: relative;
display: flex;
flex-direction: column;
}
/* 导航栏和认证相关样式已移至对应组件 */
.content-body {
position: relative;
flex: 1;
background: #fff;
min-height: 0;
overflow: hidden;
}
.dashboard-home {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
z-index: 100;
}
.icon-container { display: flex; justify-content: center; }
.main-icon {
width: 400px;
height: 400px;
border-radius: 20px;
object-fit: contain;
}
.placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.placeholder-card {
background: #ffffff;
border: 1px solid #e8eaec;
border-radius: 12px;
padding: 24px 28px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
color: #2c3e50;
}
.placeholder-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
.placeholder-desc { font-size: 13px; color: #606266; }
</style>

View File

@@ -0,0 +1,38 @@
import { http } from './http';
export const amazonApi = {
// 上传Excel文件解析ASIN列表
importAsinFromExcel(file: File) {
const formData = new FormData();
formData.append('file', file);
return http.upload<{ code: number, data: { asinList: string[], total: number }, msg: string | null }>('/api/amazon/import/asin', formData);
},
getProductsBatch(asinList: string[], batchId: string) {
return http.post<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/batch', { asinList, batchId });
},
getLatestProducts() {
return http.get<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/latest');
},
getProductsByBatch(batchId: string) {
return http.get<{ products: any[] }>(`/api/amazon/products/batch/${batchId}`);
},
updateProduct(productData: unknown) {
return http.post('/api/amazon/products/update', productData);
},
deleteProduct(productId: string) {
return http.post('/api/amazon/products/delete', { id: productId });
},
exportToExcel(products: unknown[], options: Record<string, unknown> = {}) {
return http.post('/api/amazon/export', { products, ...options });
},
getProductStats() {
return http.get('/api/amazon/stats');
},
searchProducts(searchParams: Record<string, unknown>) {
return http.get('/api/amazon/products/search', searchParams);
},
openGenmaiSpirit() {
return http.post('/api/genmai/open');
},
};

View File

@@ -0,0 +1,90 @@
import { http } from './http';
// 统一响应处理函数 - 适配ERP客户端格式
function unwrap<T>(res: any): T {
if (res && typeof res.success === 'boolean') {
if (!res.success) {
const message: string = res.message || res.msg || '请求失败';
throw new Error(message);
}
return res as T;
}
// 兼容标准格式
if (res && typeof res.code === 'number') {
if (res.code !== 0) {
const message: string = res.msg || '请求失败';
throw new Error(message);
}
return (res.data as T) ?? ({} as T);
}
return res as T;
}
// 认证相关类型定义
interface LoginRequest {
username: string;
password: string;
}
interface RegisterRequest {
username: string;
password: string;
}
interface LoginResponse {
success: boolean;
token: string;
permissions: string[];
username: string;
message?: string;
}
interface RegisterResponse {
success: boolean;
message?: string;
}
interface CheckUsernameResponse {
available: boolean;
}
export const authApi = {
// 用户登录
login(params: LoginRequest) {
return http
.post('/api/login', params)
.then(res => unwrap<LoginResponse>(res));
},
// 用户注册
register(params: RegisterRequest) {
return http
.post('/api/register', params)
.then(res => unwrap<RegisterResponse>(res));
},
// 检查用户名可用性
checkUsername(username: string) {
return http
.get('/api/check-username', { username })
.then(res => {
// checkUsername 使用标准格式 {code: 200, data: boolean}
if (res && res.code === 200) {
return { available: res.data };
}
throw new Error(res?.msg || '检查用户名失败');
});
},
// 验证token有效性
verifyToken(token: string) {
return http
.post('/api/verify', { token })
.then(res => unwrap<{ success: boolean }>(res));
},
// 用户登出
logout(token: string) {
return http.postVoid('/api/logout', { token });
},
};

View File

@@ -0,0 +1,48 @@
import { http } from './http'
// 与老版保持相同的接口路径与参数
const base = '/api/device'
export interface DeviceQuota {
limit: number
used: number
}
export interface DeviceItem {
deviceId: string
name?: string
status?: 'online' | 'offline'
lastActiveAt?: string
}
export const deviceApi = {
getQuota(username: string) {
return http.get<DeviceQuota | any>(`${base}/quota`, { username }).then((res: any) => {
if (res && typeof res.limit !== 'undefined') return res as DeviceQuota
if (res && typeof res.code === 'number') return (res.data as DeviceQuota) || { limit: 0, used: 0 }
return (res?.data as DeviceQuota) || { limit: 0, used: 0 }
})
},
list(username: string) {
return http.get<DeviceItem[] | any>(`${base}/list`, { username }).then((res: any) => {
if (Array.isArray(res)) return res as DeviceItem[]
if (res && typeof res.code === 'number') return (res.data as DeviceItem[]) || []
return (res?.data as DeviceItem[]) || []
})
},
register(payload: { username: string }) {
return http.post(`${base}/register`, payload)
},
remove(payload: { deviceId: string }) {
return http.postVoid(`${base}/remove`, payload)
},
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
return http.postVoid(`${base}/heartbeat`, payload)
},
}

View File

@@ -0,0 +1,78 @@
// 极简 HTTP 工具:仅封装 GET/POST默认指向本地 8081
export type HttpMethod = 'GET' | 'POST';
const BASE_URL = 'http://localhost:8081';
// 将对象转为查询字符串
function buildQuery(params?: Record<string, unknown>): string {
if (!params) return '';
const usp = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) return;
usp.append(key, String(value));
});
const queryString = usp.toString();
return queryString ? `?${queryString}` : '';
}
// 统一请求入口:自动加上 BASE_URL、JSON 头与错误处理
async function request<T>(path: string, options: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
credentials: 'omit',
cache: 'no-store',
...options,
headers: {
'Content-Type': 'application/json',
...(options.headers || {}),
},
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(text || `HTTP ${res.status}`);
}
const contentType = res.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return (await res.json()) as T;
}
return (await res.text()) as unknown as T;
}
export const http = {
get<T>(path: string, params?: Record<string, unknown>) {
return request<T>(`${path}${buildQuery(params)}`, { method: 'GET' });
},
post<T>(path: string, body?: unknown) {
return request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined });
},
// 用于无需读取响应体的 POST如删除/心跳等),从根源避免读取中断
postVoid(path: string, body?: unknown) {
return fetch(`${BASE_URL}${path}`, {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
credentials: 'omit',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
}).then(res => {
if (!res.ok) return res.text().then(t => Promise.reject(new Error(t || `HTTP ${res.status}`)));
return undefined as unknown as void;
});
},
// 文件上传:透传 FormData不设置 Content-Type 让浏览器自动处理
upload<T>(path: string, form: FormData) {
const res = fetch(`${BASE_URL}${path}`, {
method: 'POST',
body: form,
credentials: 'omit',
cache: 'no-store',
});
return res.then(async response => {
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(text || `HTTP ${response.status}`);
}
return response.json() as Promise<T>;
});
},
};

View File

@@ -0,0 +1,38 @@
import { http } from './http';
function unwrap<T>(res: any): T {
if (res && typeof res.code === 'number') {
if (res.code !== 0) {
const message: string = res.msg || '请求失败';
throw new Error(message);
}
return (res.data as T) ?? ({} as T);
}
return res as T;
}
export const rakutenApi = {
// 上传 Excel 或按店铺名查询
getProducts(params: { file?: File; shopName?: string; batchId?: string }) {
const formData = new FormData();
if (params.file) formData.append('file', params.file);
if (params.batchId) formData.append('batchId', params.batchId);
if (params.shopName) formData.append('shopName', params.shopName);
return http
.upload('/api/rakuten/products', formData)
.then(res => unwrap<{ products: any[]; total?: number; sessionId?: string }>(res));
},
search1688(imageUrl: string, sessionId?: string) {
const payload: Record<string, unknown> = { imageUrl };
if (sessionId) payload.sessionId = sessionId;
return http.post('/api/rakuten/search1688', payload).then(res => unwrap<any>(res));
},
getLatestProducts() {
return http.get('/api/rakuten/products/latest').then(res => unwrap<{ products: any[] }>(res));
},
exportAndSave(exportData: unknown) {
return http
.post('/api/rakuten/export-and-save', exportData)
.then(res => unwrap<{ filePath: string; fileName?: string; recordCount?: number; hasImages?: boolean }>(res));
},
};

View File

@@ -0,0 +1,15 @@
import { http } from './http';
export const shopeeApi = {
getAdHosting(params: Record<string, unknown> = {}) {
return http.get('/api/shopee/ad-hosting', params);
},
getReviews(params: Record<string, unknown> = {}) {
return http.get('/api/shopee/reviews', params);
},
exportData(exportParams: Record<string, unknown> = {}) {
return http.post('/api/shopee/export', exportParams);
},
};

View File

@@ -0,0 +1,56 @@
// 斑马订单模型(根据页面所需字段精简定义)
export interface ZebraOrder {
orderedAt?: string;
productImage?: string;
productTitle?: string;
shopOrderNumber?: string;
timeSinceOrder?: string;
priceJpy?: number;
productQuantity?: number;
shippingFeeJpy?: number;
serviceFee?: string;
productNumber?: string;
poNumber?: string;
shippingFeeCny?: number;
internationalShippingFee?: number;
poLogisticsCompany?: string;
poTrackingNumber?: string;
internationalTrackingNumber?: string;
trackInfo?: string;
}
export interface ZebraOrdersResp {
orders: ZebraOrder[];
total?: number;
totalPages?: number;
}
import { http } from './http';
// 斑马 API与原 zebra-api.js 对齐的接口封装
export const zebraApi = {
getOrders(params: Record<string, unknown>) {
return http.get<ZebraOrdersResp>('/api/banma/orders', params);
},
getOrdersByBatch(batchId: string) {
return http.get<ZebraOrdersResp>(`/api/banma/orders/batch/${batchId}`);
},
getLatestOrders() {
return http.get<ZebraOrdersResp>('/api/banma/orders/latest');
},
getShops() {
return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>('/api/banma/shops');
},
refreshToken() {
return http.post('/api/banma/refresh-token');
},
exportAndSaveOrders(exportData: unknown) {
return http.post<{ filePath: string }>('/api/banma/export-and-save', exportData);
},
getOrderStats() {
return http.get('/api/banma/orders/stats');
},
searchOrders(searchParams: Record<string, unknown>) {
return http.get('/api/banma/orders/search', searchParams);
},
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1 @@
<template></template>

View File

@@ -0,0 +1,393 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { amazonApi } from '../../api/amazon'
// 响应式状态
const loading = ref(false) // 主加载状态
const tableLoading = ref(false) // 表格加载状态
const progressPercentage = ref(0) // 进度百分比
const localProductData = ref<any[]>([]) // 本地产品数据
const singleAsin = ref('') // 单个ASIN输入
const currentAsin = ref('') // 当前处理的ASIN
const genmaiLoading = ref(false) // Genmai Spirit加载状态
// 分页配置
const currentPage = ref(1)
const pageSize = ref(15)
const totalPages = computed(() => Math.max(1, Math.ceil((localProductData.value.length || 0) / pageSize.value)))
const amazonUpload = ref<HTMLInputElement | null>(null)
const dragActive = ref(false)
// 计算属性 - 当前页数据
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return localProductData.value.slice(start, end)
})
// 通用消息提示
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
alert(`[${type.toUpperCase()}] ${message}`)
}
// Excel文件上传处理 - 主要业务逻辑入口
async function processExcelFile(file: File) {
try {
loading.value = true
tableLoading.value = true
localProductData.value = []
progressPercentage.value = 0
const response = await amazonApi.importAsinFromExcel(file)
const asinList = response.data.asinList
if (!asinList || asinList.length === 0) {
showMessage('文件中未找到有效的ASIN数据', 'warning')
return
}
showMessage(`成功解析 ${asinList.length} 个ASIN`, 'success')
await batchGetProductInfo(asinList)
} catch (error: any) {
showMessage(error.message || '处理文件失败', 'error')
} finally {
loading.value = false
tableLoading.value = false
}
}
async function handleExcelUpload(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
await processExcelFile(file)
input.value = ''
}
function onDragOver(e: DragEvent) { e.preventDefault(); dragActive.value = true }
function onDragLeave() { dragActive.value = false }
async function onDrop(e: DragEvent) {
e.preventDefault()
dragActive.value = false
const file = e.dataTransfer?.files?.[0]
if (!file) return
const ok = /(\.csv|\.txt|\.xls|\.xlsx)$/i.test(file.name)
if (!ok) return showMessage('仅支持 .csv/.txt/.xls/.xlsx 文件', 'warning')
await processExcelFile(file)
}
// 批量获取产品信息 - 核心数据处理逻辑
async function batchGetProductInfo(asinList: string[]) {
try {
currentAsin.value = '正在处理...'
progressPercentage.value = 0
localProductData.value = []
const batchId = `BATCH_${Date.now()}`
const batchSize = 2 // 每批处理2个ASIN
const totalBatches = Math.ceil(asinList.length / batchSize)
let processedCount = 0
let failedCount = 0
// 分批处理ASIN列表
for (let i = 0; i < totalBatches && loading.value; i++) {
const start = i * batchSize
const end = Math.min(start + batchSize, asinList.length)
const batchAsins = asinList.slice(start, end)
currentAsin.value = `正在处理第${i + 1}/${totalBatches}批 (${batchAsins.join(', ')})`
try {
const result = await amazonApi.getProductsBatch(batchAsins, batchId)
if (result?.data?.products?.length > 0) {
localProductData.value.push(...result.data.products)
if (tableLoading.value) tableLoading.value = false // 首次数据到达后隐藏表格加载
}
// 统计失败数量
const expectedCount = batchAsins.length
const actualCount = result?.data?.products?.length || 0
failedCount += Math.max(0, expectedCount - actualCount)
} catch (error) {
failedCount += batchAsins.length
console.error(`批次${i + 1}失败:`, error)
}
// 更新进度
processedCount += batchAsins.length
progressPercentage.value = Math.round((processedCount / asinList.length) * 100)
// 批次间延迟避免API频率限制
if (i < totalBatches - 1 && loading.value) {
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1500))
}
}
// 处理完成状态更新
progressPercentage.value = 100
currentAsin.value = '处理完成'
// 结果提示
if (failedCount > 0) {
showMessage(`采集完成!共 ${asinList.length} 个ASIN成功 ${asinList.length - failedCount} 个,失败 ${failedCount}`, 'warning')
} else {
showMessage(`采集完成!成功获取 ${asinList.length} 个产品信息`, 'success')
}
} catch (error: any) {
showMessage(error.message || '批量获取产品信息失败', 'error')
currentAsin.value = '处理失败'
} finally {
tableLoading.value = false
}
}
// 单个ASIN查询
async function searchSingleAsin() {
const asin = singleAsin.value.trim()
if (!asin) return
localProductData.value = []
loading.value = true
try {
const resp = await amazonApi.getProductsBatch([asin], `SINGLE_${Date.now()}`)
if (resp?.data?.products?.length > 0) {
localProductData.value = resp.data.products
showMessage('查询成功', 'success')
singleAsin.value = ''
} else {
showMessage('未找到商品信息', 'warning')
}
} catch (e: any) {
showMessage(e?.message || '查询失败', 'error')
} finally {
loading.value = false
}
}
// 导出Excel数据
async function exportToExcel() {
if (!localProductData.value.length) {
showMessage('没有数据可供导出', 'warning')
return
}
try {
loading.value = true
showMessage('正在生成Excel文件请稍候...', 'info')
// 数据格式化 - 只保留核心字段
const exportData = localProductData.value.map(product => ({
asin: product.asin || '',
seller_shipper: getSellerShipperText(product),
price: product.price || '无货'
}))
await amazonApi.exportToExcel(exportData, {
filename: `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
})
showMessage('Excel文件导出成功', 'success')
} catch (error: any) {
showMessage(error.message || '导出Excel失败', 'error')
} finally {
loading.value = false
}
}
// 获取卖家/配送方信息 - 数据处理辅助函数
function getSellerShipperText(product: any) {
let text = product.seller || '无货'
if (product.shipper && product.shipper !== product.seller) {
text += (text && text !== '无货' ? ' / ' : '') + product.shipper
}
return text
}
// 停止获取操作
function stopFetch() {
loading.value = false
currentAsin.value = '已停止'
showMessage('已停止获取产品数据', 'info')
}
// 打开Genmai Spirit工具
async function openGenmaiSpirit() {
genmaiLoading.value = true
try {
await amazonApi.openGenmaiSpirit()
} catch (error: any) {
showMessage(error.message || '打开跟卖精灵失败', 'error')
} finally {
genmaiLoading.value = false
}
}
// 分页处理
function handleSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
}
function handleCurrentChange(page: number) {
currentPage.value = page
}
// 使用 Element Plus 的 jumper不再需要手动跳转函数
function openAmazonUpload() {
amazonUpload.value?.click()
}
// 组件挂载时获取最新数据
onMounted(async () => {
try {
const resp = await amazonApi.getLatestProducts()
localProductData.value = resp.data?.products || []
} catch {
// 静默处理初始化失败
}
})
</script>
<template>
<div class="amazon-root">
<div class="main-container">
<!-- 文件导入和操作区域 -->
<div class="import-section" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" :class="{ 'drag-active': dragActive }">
<div class="import-controls">
<!-- 文件上传按钮 -->
<el-button type="primary" :disabled="loading" @click="openAmazonUpload">
📂 {{ loading ? '处理中...' : '导入ASIN列表' }}
</el-button>
<input ref="amazonUpload" style="display:none" type="file" accept=".csv,.txt,.xls,.xlsx" @change="handleExcelUpload" :disabled="loading" />
<!-- 单个ASIN输入 -->
<div class="single-input">
<input class="text" v-model="singleAsin" placeholder="输入单个ASIN" :disabled="loading" @keyup.enter="searchSingleAsin" />
<el-button type="info" :disabled="!singleAsin || loading" @click="searchSingleAsin">查询</el-button>
</div>
<!-- 操作按钮组 -->
<div class="action-buttons">
<el-button type="danger" :disabled="!loading" @click="stopFetch">停止获取</el-button>
<el-button type="success" :disabled="!localProductData.length || loading" @click="exportToExcel">导出Excel</el-button>
<el-button type="warning" :loading="genmaiLoading" @click="openGenmaiSpirit">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</el-button>
</div>
</div>
<!-- 进度条显示 -->
<div class="progress-section" v-if="loading">
<div class="progress-box">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
</div>
<div class="progress-text">{{ progressPercentage }}%</div>
</div>
<div class="current-status" v-if="currentAsin">{{ currentAsin }}</div>
</div>
</div>
</div>
<!-- 数据显示区域 -->
<div class="table-container">
<!-- 数据表格无数据时也显示表头 -->
<div class="table-section">
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th width="130">ASIN</th>
<th>卖家/配送方</th>
<th width="120">当前售价</th>
</tr>
</thead>
<tbody>
<tr v-if="paginatedData.length === 0">
<td colspan="3" class="empty-tip">暂无数据请导入ASIN列表</td>
</tr>
<tr v-else v-for="row in paginatedData" :key="row.asin">
<td>{{ row.asin }}</td>
<td>
<div class="seller-info">
<span class="seller">{{ row.seller || '无货' }}</span>
<span v-if="row.shipper && row.shipper !== row.seller" class="shipper">/ {{ row.shipper }}</span>
</div>
</td>
<td>
<span class="price">{{ row.price || '无货' }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 表格加载遮罩 -->
<div v-if="tableLoading" class="table-loading">
<div class="spinner"></div>
<div>加载中...</div>
</div>
</div>
<!-- 分页器 -->
<div class="pagination-fixed" >
<el-pagination
background
:current-page="currentPage"
:page-sizes="[15,30,50,100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="localProductData.length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.amazon-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
.import-section { margin-bottom: 10px; flex-shrink: 0; }
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; margin-bottom: 8px; }
.single-input { display: flex; align-items: center; gap: 8px; }
.text { width: 180px; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; outline: none; transition: border-color 0.2s ease; }
.text:focus { border-color: #409EFF; }
.text:disabled { background: #f5f7fa; color: #c0c4cc; }
.action-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
.progress-section { margin: 15px 0 10px 0; }
.progress-box { padding: 8px 0; }
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
.progress-bar { flex: 1; height: 6px; background: #ebeef5; border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 3px; transition: width 0.3s ease; }
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
.table-wrapper { height: 100%; overflow: auto; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
.table tbody tr:hover { background: #f9f9f9; }
.seller-info { display: flex; align-items: center; gap: 4px; }
.seller { color: #303133; font-weight: 500; }
.shipper { color: #909399; font-size: 12px; }
.price { color: #e6a23c; font-weight: 600; }
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
.empty-tip { text-align: center; color: #909399; padding: 16px 0; }
.import-section[draggable], .import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
</style>
<script lang="ts">
export default {
name: 'AmazonDashboard',
}
</script>

View File

@@ -0,0 +1,124 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { User } from '@element-plus/icons-vue'
import { authApi } from '../../api/auth'
interface Props {
modelValue: boolean
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'loginSuccess', data: { token: string; user: any }): void
(e: 'showRegister'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const authForm = ref({ username: '', password: '' })
const authLoading = ref(false)
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
async function handleAuth() {
if (!authForm.value.username || !authForm.value.password) return
authLoading.value = true
try {
const data = await authApi.login(authForm.value)
localStorage.setItem('token', data.token)
emit('loginSuccess', {
token: data.token,
user: {
username: data.username,
permissions: data.permissions
}
})
ElMessage.success('登录成功')
resetForm()
} catch (err) {
ElMessage.error((err as Error).message)
} finally {
authLoading.value = false
}
}
function cancelAuth() {
visible.value = false
resetForm()
}
function resetForm() {
authForm.value = { username: '', password: '' }
}
function showRegister() {
emit('showRegister')
}
</script>
<template>
<el-dialog
title="用户登录"
v-model="visible"
:close-on-click-modal="false"
width="400px"
center>
<div style="text-align: center; padding: 20px 0;">
<div style="margin-bottom: 30px; color: #666;">
<el-icon size="48" color="#409EFF"><User /></el-icon>
<p style="margin-top: 15px; font-size: 16px;">请登录以使用系统功能</p>
</div>
<el-input
v-model="authForm.username"
placeholder="请输入用户名"
prefix-icon="User"
size="large"
style="margin-bottom: 15px;"
:disabled="authLoading"
@keyup.enter="handleAuth">
</el-input>
<el-input
v-model="authForm.password"
placeholder="请输入密码"
type="password"
size="large"
style="margin-bottom: 20px;"
:disabled="authLoading"
@keyup.enter="handleAuth">
</el-input>
<div>
<el-button
type="primary"
size="large"
:loading="authLoading"
:disabled="!authForm.username || !authForm.password || authLoading"
@click="handleAuth"
style="width: 120px; margin-right: 10px;">
登录
</el-button>
<el-button
size="large"
:disabled="authLoading"
@click="cancelAuth"
style="width: 120px;">
取消
</el-button>
</div>
<div style="margin-top: 20px; text-align: center;">
<el-button type="text" @click="showRegister" :disabled="authLoading">
还没有账号点击注册
</el-button>
</div>
</div>
</el-dialog>
</template>

View File

@@ -0,0 +1,162 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { User } from '@element-plus/icons-vue'
import { authApi } from '../../api/auth'
interface Props {
modelValue: boolean
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'registerSuccess'): void
(e: 'backToLogin'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const registerForm = ref({ username: '', password: '', confirmPassword: '' })
const registerLoading = ref(false)
const usernameCheckResult = ref<boolean | null>(null)
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const canRegister = computed(() => {
const { username, password, confirmPassword } = registerForm.value
return username &&
password.length >= 6 &&
password === confirmPassword &&
usernameCheckResult.value === true
})
async function checkUsernameAvailability() {
if (!registerForm.value.username) {
usernameCheckResult.value = null
return
}
try {
const data = await authApi.checkUsername(registerForm.value.username)
usernameCheckResult.value = data.available
} catch {
usernameCheckResult.value = null
}
}
async function handleRegister() {
if (!canRegister.value) return
registerLoading.value = true
try {
const result = await authApi.register({
username: registerForm.value.username,
password: registerForm.value.password
})
ElMessage.success(result.message || '注册成功,请登录')
emit('registerSuccess')
resetForm()
} catch (err) {
ElMessage.error((err as Error).message)
} finally {
registerLoading.value = false
}
}
function cancelRegister() {
visible.value = false
resetForm()
}
function resetForm() {
registerForm.value = { username: '', password: '', confirmPassword: '' }
usernameCheckResult.value = null
}
function backToLogin() {
emit('backToLogin')
resetForm()
}
</script>
<template>
<el-dialog
title="账号注册"
v-model="visible"
:close-on-click-modal="false"
width="450px"
center>
<div style="text-align: center; padding: 20px 0;">
<div style="margin-bottom: 20px; color: #666;">
<el-icon size="48" color="#67C23A"><User /></el-icon>
<p style="margin-top: 15px; font-size: 16px;">创建新账号</p>
</div>
<el-input
v-model="registerForm.username"
placeholder="请输入用户名"
prefix-icon="User"
size="large"
style="margin-bottom: 15px;"
:disabled="registerLoading"
@blur="checkUsernameAvailability">
</el-input>
<div v-if="usernameCheckResult !== null" style="margin-bottom: 15px; text-align: left;">
<span v-if="usernameCheckResult" style="color: #67C23A; font-size: 12px;">
用户名可用
</span>
<span v-else style="color: #F56C6C; font-size: 12px;">
用户名已存在
</span>
</div>
<el-input
v-model="registerForm.password"
placeholder="请输入密码至少6位"
type="password"
size="large"
style="margin-bottom: 15px;"
:disabled="registerLoading">
</el-input>
<el-input
v-model="registerForm.confirmPassword"
placeholder="请再次输入密码"
type="password"
size="large"
style="margin-bottom: 20px;"
:disabled="registerLoading">
</el-input>
<div>
<el-button
type="success"
size="large"
:loading="registerLoading"
:disabled="!canRegister || registerLoading"
@click="handleRegister"
style="width: 120px; margin-right: 10px;">
注册
</el-button>
<el-button
size="large"
:disabled="registerLoading"
@click="cancelRegister"
style="width: 120px;">
取消
</el-button>
</div>
<div style="margin-top: 20px; text-align: center;">
<el-button type="text" @click="backToLogin" :disabled="registerLoading">
已有账号返回登录
</el-button>
</div>
</div>
</el-dialog>
</template>

View File

@@ -0,0 +1,172 @@
<script setup lang="ts">
import { ArrowLeft, ArrowRight, Refresh, Monitor, Setting, User } from '@element-plus/icons-vue'
interface Props {
canGoBack: boolean
canGoForward: boolean
activeMenu: string
}
interface Emits {
(e: 'go-back'): void
(e: 'go-forward'): void
(e: 'reload'): void
(e: 'user-click'): void
(e: 'open-device'): void
}
defineProps<Props>()
defineEmits<Emits>()
</script>
<template>
<div class="top-navbar">
<div class="navbar-left">
<div class="nav-controls">
<button class="nav-btn" title="后退" @click="$emit('go-back')" :disabled="!canGoBack">
<el-icon><ArrowLeft /></el-icon>
</button>
<button class="nav-btn" title="前进" @click="$emit('go-forward')" :disabled="!canGoForward">
<el-icon><ArrowRight /></el-icon>
</button>
</div>
</div>
<div class="navbar-center">
<div class="breadcrumbs">
<span>首页</span>
<span class="separator">></span>
<span>{{ activeMenu }}</span>
</div>
</div>
<div class="navbar-right">
<button class="nav-btn-round" title="刷新" @click="$emit('reload')">
<el-icon><Refresh /></el-icon>
</button>
<button class="nav-btn-round" title="设备管理" @click="$emit('open-device')">
<el-icon><Monitor /></el-icon>
</button>
<button class="nav-btn-round" title="设置">
<el-icon><Setting /></el-icon>
</button>
<button class="nav-btn-round" title="用户" @click="$emit('user-click')">
<el-icon><User /></el-icon>
</button>
</div>
</div>
</template>
<style scoped>
.top-navbar {
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: #ffffff;
border-bottom: 1px solid #e8eaec;
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
}
.navbar-left {
display: flex;
align-items: center;
flex: 0 0 auto;
}
.navbar-center {
display: flex;
justify-content: center;
flex: 1;
}
.navbar-right {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
}
.nav-controls {
display: flex;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.nav-btn {
width: 36px;
height: 32px;
border: none;
background: #fff;
cursor: pointer;
font-size: 16px;
color: #606266;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
outline: none;
}
.nav-btn:hover:not(:disabled) {
background: #f5f7fa;
color: #409EFF;
}
.nav-btn:focus,
.nav-btn:active {
outline: none;
border: none;
}
.nav-btn:disabled {
cursor: not-allowed;
opacity: 0.5;
background: #f5f5f5;
color: #c0c4cc;
}
.nav-btn:not(:last-child) {
border-right: 1px solid #dcdfe6;
}
.nav-btn-round {
width: 32px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 50%;
background: #fff;
cursor: pointer;
font-size: 14px;
color: #606266;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
outline: none;
}
.nav-btn-round:hover {
background: #f5f7fa;
color: #409EFF;
border-color: #c6e2ff;
}
.nav-btn-round:focus,
.nav-btn-round:active {
outline: none;
}
.breadcrumbs {
display: flex;
align-items: center;
color: #606266;
font-size: 14px;
}
.separator {
margin: 0 8px;
color: #c0c4cc;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,632 @@
<script setup lang="ts">
import {ref, computed, onMounted} from 'vue'
import {rakutenApi} from '../../api/rakuten'
// UI 与加载状态
const loading = ref(false)
const tableLoading = ref(false)
const exportLoading = ref(false)
const statusMessage = ref('')
const statusType = ref<'info' | 'success' | 'warning' | 'error'>('info')
// 查询与上传
const singleShopName = ref('')
const currentBatchId = ref('')
const uploadInputRef = ref<HTMLInputElement | null>(null)
const dragActive = ref(false)
// 数据与分页
const allProducts = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(15)
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return allProducts.value.slice(start, end)
})
// 进度(完成后仍保持显示)
const progressStarted = ref(false)
const progressPercentage = ref(0)
const totalProducts = ref(0)
const processedProducts = ref(0)
function handleSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
}
function handleCurrentChange(page: number) {
currentPage.value = page
}
function openRakutenUpload() {
uploadInputRef.value?.click()
}
function parseSkuPrices(product: any) {
if (!product.skuPrice) return []
try {
let skuStr = product.skuPrice
if (typeof skuStr === 'string') {
skuStr = skuStr.replace(/(\d+(?:\.\d+)?):"/g, '"$1":"')
skuStr = JSON.parse(skuStr)
}
return Object.keys(skuStr).map(p => parseFloat(p)).filter(n => !isNaN(n)).sort((a, b) => a - b)
} catch {
return []
}
}
async function loadLatest() {
const resp = await rakutenApi.getLatestProducts()
allProducts.value = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
}
async function searchProductInternal(product: any) {
if (!product || !product.imgUrl) return
if (product.mapRecognitionLink && String(product.mapRecognitionLink).trim() !== '') return
const res = await rakutenApi.search1688(product.imgUrl, currentBatchId.value)
const data = res
Object.assign(product, {
mapRecognitionLink: data.mapRecognitionLink,
freight: data.freight,
median: data.median,
weight: data.weight,
skuPrice: data.skuPrice,
skuPrices: parseSkuPrices(data),
image1688Url: data.mapRecognitionLink,
detailUrl1688: data.mapRecognitionLink,
})
}
function beforeUpload(file: File) {
const ok = /\.xlsx?$/.test(file.name)
if (!ok) alert('仅支持 .xlsx/.xls 文件')
return ok
}
async function processFile(file: File) {
if (!beforeUpload(file)) return
progressStarted.value = true
progressPercentage.value = 0
totalProducts.value = 0
processedProducts.value = 0
loading.value = true
tableLoading.value = true
currentBatchId.value = `RAKUTEN_${Date.now()}`
try {
const resp = await rakutenApi.getProducts({file, batchId: currentBatchId.value})
const products = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
allProducts.value = products
statusMessage.value = `已获取 ${allProducts.value.length} 个乐天商品`
const needSearch = allProducts.value.filter(p => p && p.imgUrl && !p.mapRecognitionLink)
if (needSearch.length > 0) {
statusType.value = 'info'
statusMessage.value = `已获取 ${allProducts.value.length} 个乐天商品正在自动获取1688数据...`
await startBatch1688Search(needSearch)
} else {
statusType.value = 'success'
statusMessage.value = `已获取 ${allProducts.value.length} 个乐天商品,所有数据已完整!`
}
} catch (e: any) {
statusMessage.value = e?.message || '上传失败'
statusType.value = 'error'
} finally {
loading.value = false
tableLoading.value = false
}
}
async function handleExcelUpload(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files && input.files[0]
if (!file) return
await processFile(file)
input.value = ''
}
function onDragOver(e: DragEvent) { e.preventDefault(); dragActive.value = true }
function onDragLeave() { dragActive.value = false }
async function onDrop(e: DragEvent) {
e.preventDefault()
dragActive.value = false
const file = e.dataTransfer?.files?.[0]
if (!file) return
await processFile(file)
}
async function searchSingleShop() {
const shop = singleShopName.value.trim()
if (!shop) return
// 重置进度与状态
progressStarted.value = true
progressPercentage.value = 0
totalProducts.value = 0
processedProducts.value = 0
loading.value = true
tableLoading.value = true
currentBatchId.value = `RAKUTEN_${Date.now()}`
try {
const resp = await rakutenApi.getProducts({shopName: shop, batchId: currentBatchId.value})
allProducts.value = (resp.products || []).filter((p: any) => p.originalShopName === shop).map(p => ({ ...p, skuPrices: parseSkuPrices(p) }))
statusMessage.value = `店铺 ${shop}${allProducts.value.length}`
singleShopName.value = ''
const needSearch = allProducts.value.filter(p => p && p.imgUrl && !p.mapRecognitionLink)
if (needSearch.length > 0) {
await startBatch1688Search(needSearch)
} else if (allProducts.value.length > 0) {
statusType.value = 'success'
statusMessage.value = `店铺 ${shop} 的数据已加载完成所有1688链接都已存在`
progressPercentage.value = 100
}
} catch (e: any) {
statusMessage.value = e?.message || '查询失败'
statusType.value = 'error'
} finally {
loading.value = false
tableLoading.value = false
}
}
function stopTask() {
loading.value = false
tableLoading.value = false
statusType.value = 'warning'
statusMessage.value = '任务已停止'
// 保留进度条和当前进度
allProducts.value = allProducts.value.map(p => ({...p, searching1688: false}))
}
async function startBatch1688Search(products: any[]) {
const items = (products || []).filter(p => p && p.imgUrl && !p.mapRecognitionLink)
if (items.length === 0) {
progressPercentage.value = 100
statusType.value = 'success'
statusMessage.value = '所有商品都已获取1688数据'
return
}
loading.value = true
totalProducts.value = items.length
processedProducts.value = 0
progressStarted.value = true
progressPercentage.value = 0
statusType.value = 'info'
statusMessage.value = `正在获取1688数据${totalProducts.value} 个商品...`
await serialSearch1688(items)
if (processedProducts.value >= totalProducts.value) {
progressPercentage.value = 100
statusType.value = 'success'
const successCount = allProducts.value.filter(p => p && p.mapRecognitionLink && String(p.mapRecognitionLink).trim() !== '').length
statusMessage.value = `成功获取 ${successCount}`
}
loading.value = false
}
async function serialSearch1688(products: any[]) {
for (let i = 0; i < products.length && loading.value; i++) {
const product = products[i]
product.searching1688 = true
await nextTickSafe()
await searchProductInternal(product)
product.searching1688 = false
processedProducts.value++
progressPercentage.value = Math.floor((processedProducts.value / Math.max(1, totalProducts.value)) * 100)
if (i < products.length - 1 && loading.value) {
await delay(500 + Math.random() * 1000)
}
}
}
function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
function nextTickSafe() {
// 不额外引入 nextTick使用微任务刷新即可保持体积精简
return Promise.resolve()
}
async function exportToExcel() {
try {
if (allProducts.value.length === 0) return alert('没有数据可导出')
exportLoading.value = true
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
const fileName = `乐天商品数据_${timestamp}.xlsx`
const payload = {
products: allProducts.value,
title: '乐天商品数据导出',
fileName,
timestamp: new Date().toLocaleString('zh-CN'),
// 传给后端的可选提示参数
useMultiThread: true,
chunkSize: 300,
skipImages: allProducts.value.length > 200,
}
const resp = await rakutenApi.exportAndSave(payload)
alert(`Excel文件已保存到: ${resp.filePath}`)
} catch (e: any) {
alert(e?.message || '导出失败')
} finally {
exportLoading.value = false
}
}
onMounted(loadLatest)
</script>
<template>
<div class="rakuten-root">
<div class="main-container">
<!-- 文件导入和操作区域 -->
<div class="import-section" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" :class="{ 'drag-active': dragActive }">
<div class="import-controls">
<!-- 文件上传按钮 -->
<el-button type="primary" :disabled="loading" @click="openRakutenUpload">
📂 {{ loading ? '处理中...' : '导入店铺名列表' }}
</el-button>
<input ref="uploadInputRef" style="display:none" type="file" accept=".xlsx,.xls" @change="handleExcelUpload"
:disabled="loading"/>
<!-- 单个店铺名输入 -->
<div class="single-input">
<el-input v-model="singleShopName" placeholder="输入单个店铺名" :disabled="loading"
@keyup.enter="searchSingleShop" style="width: 140px"/>
<el-button type="info" :disabled="!singleShopName || loading" @click="searchSingleShop">查询</el-button>
</div>
<!-- 操作按钮组 -->
<div class="action-buttons">
<el-button type="danger" :disabled="!loading" @click="stopTask">停止获取</el-button>
<el-button type="success" :disabled="!allProducts.length || loading" @click="exportToExcel">导出Excel
</el-button>
</div>
</div>
<!-- 进度条显示 -->
<div class="progress-section" v-if="progressStarted">
<div class="progress-box">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
</div>
<div class="progress-text">{{ progressPercentage }}%</div>
</div>
<div class="current-status" v-if="statusMessage">{{ statusMessage }}</div>
</div>
</div>
</div>
<!-- 数据显示区域 -->
<div class="table-container">
<!-- 数据表格无数据时也显示表头 -->
<div class="table-section">
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>店铺名</th>
<th>商品链接</th>
<th>商品图片</th>
<th>排名</th>
<th>商品标题</th>
<th>价格</th>
<th>1688识图链接</th>
<th>1688运费</th>
<th>1688中位价</th>
<th>1688最低价</th>
<th>1688中间价</th>
<th>1688最高价</th>
</tr>
</thead>
<tbody>
<tr v-if="paginatedData.length === 0">
<td colspan="12" class="empty-tip">暂无数据请导入店铺名列表</td>
</tr>
<tr v-else v-for="row in paginatedData" :key="row.productUrl + (row.productTitle || '')">
<td class="truncate shop-col" :title="row.originalShopName">{{ row.originalShopName }}</td>
<td class="truncate url-col">
<el-input v-if="row.productUrl" :value="row.productUrl" readonly @click="$event.target.select()" size="small"/>
<span v-else>--</span>
</td>
<td>
<div class="image-container" v-if="row.imgUrl">
<img :src="row.imgUrl" class="thumb" alt="thumb"/>
</div>
<span v-else>无图片</span>
</td>
<td>
<span v-if="row.ranking">{{ row.ranking }}</span>
<span v-else>--</span>
</td>
<td class="truncate" :title="row.productTitle">{{ row.productTitle || '--' }}</td>
<td>{{ row.price ? row.price + '円' : '--' }}</td>
<td class="truncate url-col">
<el-input v-if="row.mapRecognitionLink" :value="row.mapRecognitionLink" readonly @click="$event.target.select()" size="small"/>
<span v-else-if="row.searching1688">搜索中...</span>
<span v-else>--</span>
</td>
<td>{{ row.freight ?? '--' }}</td>
<td>{{ row.median ?? '--' }}</td>
<td>{{ row.skuPrices?.[0] ?? '--' }}</td>
<td>{{ row.skuPrices?.[Math.floor(row.skuPrices.length / 2)] ?? '--' }}</td>
<td>{{ row.skuPrices?.[row.skuPrices.length - 1] ?? '--' }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 表格加载遮罩 -->
<div v-if="tableLoading" class="table-loading">
<div class="spinner"></div>
<div>加载中...</div>
</div>
</div>
<!-- 分页器 -->
<div class="pagination-fixed" >
<el-pagination
background
:current-page="currentPage"
:page-sizes="[15,30,50,100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="allProducts.length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.rakuten-root {
position: absolute;
inset: 0;
background: #f5f5f5;
padding: 12px;
box-sizing: border-box;
}
.main-container {
background: #fff;
border-radius: 4px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
height: 100%;
display: flex;
flex-direction: column;
}
.import-section {
margin-bottom: 10px;
flex-shrink: 0;
}
.import-controls {
display: flex;
align-items: flex-end;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.single-input {
display: flex;
align-items: center;
gap: 8px;
}
.action-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.progress-section {
margin: 15px 0 10px 0;
}
.progress-box {
padding: 8px 0;
}
.progress-container {
display: flex;
align-items: center;
position: relative;
padding-right: 50px;
margin-bottom: 8px;
}
.progress-bar {
flex: 1;
height: 6px;
background: #ebeef5;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #409EFF, #66b1ff);
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-text {
position: absolute;
right: 0;
font-size: 13px;
color: #409EFF;
font-weight: 500;
}
.current-status {
font-size: 12px;
color: #606266;
padding-left: 2px;
}
.table-container {
display: flex;
flex-direction: column;
flex: 1;
min-height: 400px;
overflow: hidden;
}
.empty-section {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 6px;
}
.empty-container {
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
}
.empty-text {
font-size: 14px;
color: #909399;
}
.table-section {
flex: 1;
overflow: hidden;
position: relative;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.table-wrapper {
height: 100%;
overflow: auto;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.table th {
background: #f5f7fa;
color: #909399;
font-weight: 600;
padding: 8px 6px;
border-bottom: 2px solid #ebeef5;
text-align: left;
font-size: 12px;
white-space: nowrap;
}
.table td {
padding: 10px 8px;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
}
.table tbody tr:hover {
background: #f9f9f9;
}
.truncate {
max-width: 260px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shop-col { max-width: 160px; }
.url-col { max-width: 220px; }
.empty-tip { text-align: center; color: #909399; padding: 16px 0; }
.import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
.image-container {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
margin: 0 auto;
background: #f8f9fa;
border-radius: 2px;
}
.thumb {
width: 32px;
height: 32px;
object-fit: contain;
border-radius: 2px;
}
.table-loading {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 14px;
color: #606266;
}
.spinner {
font-size: 24px;
animation: spin 1s linear infinite;
margin-bottom: 8px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.pagination-fixed {
flex-shrink: 0;
padding: 8px 12px;
background: #f9f9f9;
border-radius: 4px;
display: flex;
justify-content: center;
border-top: 1px solid #ebeef5;
margin-top: 8px;
}
</style>
<script lang="ts">
export default {
name: 'RakutenDashboard',
}
</script>

View File

@@ -0,0 +1,322 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { zebraApi, type ZebraOrder } from '../../api/zebra'
type Shop = { id: string; shopName: string }
const shopList = ref<Shop[]>([])
const selectedShops = ref<string[]>([])
const dateRange = ref<string[]>([])
const loading = ref(false)
const exportLoading = ref(false)
const progressPercentage = ref(0)
const showProgress = ref(false)
const allOrderData = ref<ZebraOrder[]>([])
const currentPage = ref(1)
const pageSize = ref(15)
// 批量获取状态
const fetchCurrentPage = ref(1)
const fetchTotalPages = ref(0)
const fetchTotalItems = ref(0)
const isFetching = ref(false)
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return allOrderData.value.slice(start, end)
})
function formatJpy(v?: number) {
const n = Number(v || 0)
return `¥${n.toLocaleString('ja-JP')}`
}
function formatCny(v?: number) {
const n = Number(v || 0)
return `¥${n.toLocaleString('zh-CN')}`
}
async function loadShops() {
try {
const resp = await zebraApi.getShops()
const list = (resp as any)?.data?.data?.list ?? (resp as any)?.list ?? []
shopList.value = list
} catch (e) {
console.error('获取店铺列表失败:', e)
}
}
function handleSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
}
function handleCurrentChange(page: number) {
currentPage.value = page
}
async function fetchData() {
if (isFetching.value) return
loading.value = true
isFetching.value = true
showProgress.value = true
progressPercentage.value = 0
allOrderData.value = []
fetchCurrentPage.value = 1
fetchTotalItems.value = 0
const [startDate = '', endDate = ''] = dateRange.value || []
await fetchPageData(startDate, endDate)
}
async function fetchPageData(startDate: string, endDate: string) {
if (!isFetching.value) return
try {
const data = await zebraApi.getOrders({
startDate,
endDate,
page: fetchCurrentPage.value,
pageSize: 50,
shopIds: selectedShops.value.join(',')
})
const orders = data.orders || []
allOrderData.value = [...allOrderData.value, ...orders]
fetchTotalPages.value = data.totalPages || 0
fetchTotalItems.value = data.total || 0
if (fetchCurrentPage.value < fetchTotalPages.value && isFetching.value) {
progressPercentage.value = Math.round((fetchCurrentPage.value / fetchTotalPages.value) * 100)
fetchCurrentPage.value++
setTimeout(() => fetchPageData(startDate, endDate), 200)
} else {
progressPercentage.value = 100
finishFetching()
}
} catch (e) {
console.error('获取订单数据失败:', e)
finishFetching()
}
}
function finishFetching() {
isFetching.value = false
loading.value = false
// 确保进度条完全填满
progressPercentage.value = 100
currentPage.value = 1
// 进度条保留显示,不自动隐藏
}
function stopFetch() {
isFetching.value = false
loading.value = false
// 进度条保留显示,不自动隐藏
}
async function exportToExcel() {
if (!allOrderData.value.length) return
exportLoading.value = true
try {
const result = await zebraApi.exportAndSaveOrders({ orders: allOrderData.value })
alert(`Excel文件已保存到: ${result.filePath}`)
} catch (e) {
alert('导出Excel失败')
} finally {
exportLoading.value = false
}
}
onMounted(async () => {
await loadShops()
try {
const latest = await zebraApi.getLatestOrders()
allOrderData.value = latest?.orders || []
} catch {}
})
</script>
<template>
<div class="zebra-root">
<div class="main-container">
<!-- 筛选和操作区域 -->
<div class="import-section">
<div class="import-controls">
<!-- 店铺选择 -->
<el-select v-model="selectedShops" multiple placeholder="选择店铺" style="width: 260px;" :disabled="loading">
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.shopName" :value="shop.id"></el-option>
</el-select>
<!-- 日期选择 -->
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 200px;"
:disabled="loading"
/>
<!-- 操作按钮组 -->
<div class="action-buttons">
<el-button type="primary" :disabled="loading" @click="fetchData">
📂 {{ loading ? '处理中...' : '获取订单数据' }}
</el-button>
<el-button type="danger" :disabled="!loading" @click="stopFetch">停止获取</el-button>
<el-button type="success" :disabled="exportLoading || !allOrderData.length" @click="exportToExcel">导出Excel</el-button>
</div>
</div>
<!-- 进度条显示 -->
<div class="progress-section" v-if="showProgress">
<div class="progress-box">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
</div>
<div class="progress-text">{{ progressPercentage }}%</div>
</div>
<div class="current-status" v-if="fetchTotalItems > 0">
{{ progressPercentage >= 100 ? '完成' : `获取中... (${allOrderData.length}/${fetchTotalItems})` }}
</div>
</div>
</div>
</div>
<!-- 数据显示区域 -->
<div class="table-container">
<!-- 数据表格无数据时也显示表头 -->
<div class="table-section">
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>下单时间</th>
<th>商品图片</th>
<th>商品名称</th>
<th>乐天订单号</th>
<th>下单距今</th>
<th>订单金额/日元</th>
<th>数量</th>
<th>税费/日元</th>
<th>回款抽点rmb</th>
<th>商品番号</th>
<th>1688订单号</th>
<th>采购金额/rmb</th>
<th>国际运费/rmb</th>
<th>国内物流</th>
<th>国内单号</th>
<th>日本单号</th>
<th>地址状态</th>
</tr>
</thead>
<tbody>
<tr v-if="paginatedData.length === 0">
<td colspan="16" class="empty-tip">暂无数据请选择日期范围获取订单</td>
</tr>
<tr v-else v-for="row in paginatedData" :key="row.shopOrderNumber + (row.productNumber || '')">
<td>{{ row.orderedAt || '-' }}</td>
<td>
<div class="image-container" v-if="row.productImage">
<img :src="row.productImage" class="thumb" alt="thumb" />
</div>
<span v-else>无图片</span>
</td>
<td class="truncate" :title="row.productTitle">{{ row.productTitle }}</td>
<td class="truncate" :title="row.shopOrderNumber">{{ row.shopOrderNumber }}</td>
<td>{{ row.timeSinceOrder || '-' }}</td>
<td><span class="price-tag">{{ formatJpy(row.priceJpy) }}</span></td>
<td>{{ row.productQuantity || 0 }}</td>
<td><span class="fee-tag">{{ formatJpy(row.shippingFeeJpy) }}</span></td>
<td>{{ row.serviceFee || '-' }}</td>
<td class="truncate" :title="row.productNumber">{{ row.productNumber }}</td>
<td class="truncate" :title="row.poNumber">{{ row.poNumber }}</td>
<td><span class="fee-tag">{{ formatCny(row.shippingFeeCny) }}</span></td>
<td>{{ row.internationalShippingFee || '-' }}</td>
<td>{{ row.poLogisticsCompany || '-' }}</td>
<td class="truncate" :title="row.poTrackingNumber">{{ row.poTrackingNumber }}</td>
<td class="truncate" :title="row.internationalTrackingNumber">{{ row.internationalTrackingNumber }}</td>
<td>
<span v-if="row.trackInfo" class="tag">{{ row.trackInfo }}</span>
<span v-else>暂无</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 表格加载遮罩 -->
<div v-if="loading && !allOrderData.length" class="table-loading">
<div class="spinner"></div>
<div>加载中...</div>
</div>
</div>
<!-- 分页器 -->
<div class="pagination-fixed">
<el-pagination
background
:current-page="currentPage"
:page-sizes="[15,30,50,100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="allOrderData.length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'ZebraDashboard',
}
</script>
<style scoped>
.zebra-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
.import-section { margin-bottom: 10px; flex-shrink: 0; }
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; margin-bottom: 8px; }
.action-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
.progress-section { margin: 15px 0 10px 0; }
.progress-box { padding: 8px 0; }
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
.progress-bar { flex: 1; height: 6px; background: #ebeef5; border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 3px; transition: width 0.3s ease; }
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
.empty-section { flex: 1; display: flex; justify-content: center; align-items: center; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; }
.empty-container { text-align: center; }
.empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.6; }
.empty-text { font-size: 14px; color: #909399; }
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
.table-wrapper { height: 100%; overflow: auto; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
.table tbody tr:hover { background: #f9f9f9; }
.truncate { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.image-container { display: flex; justify-content: center; align-items: center; width: 24px; height: 20px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
.thumb { width: 16px; height: 16px; object-fit: contain; border-radius: 2px; }
.price-tag { color: #e6a23c; font-weight: bold; }
.fee-tag { color: #909399; font-weight: 500; }
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
.tag { display: inline-block; padding: 2px 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
</style>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vite + Vue template</title>
<link rel="icon" href="/icon/icon.png">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,9 @@
import { createApp } from 'vue'
import './style.css';
import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

View File

@@ -0,0 +1,89 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"]
},
"include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"],
}

View File

@@ -0,0 +1,12 @@
/**
* Should match main/preload.ts for typescript support in renderer
*/
export default interface ElectronApi {
sendMessage: (message: string) => void
}
declare global {
interface Window {
electronAPI: ElectronApi,
}
}

View File

@@ -0,0 +1,6 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -0,0 +1,23 @@
const Path = require('path');
const vuePlugin = require('@vitejs/plugin-vue')
const { defineConfig } = require('vite');
/**
* https://vitejs.dev/config
*/
const config = defineConfig({
root: Path.join(__dirname, 'src', 'renderer'),
publicDir: Path.join(__dirname, 'public'),
server: {
port: 8083,
},
open: false,
build: {
outDir: Path.join(__dirname, 'build', 'renderer'),
emptyOutDir: true,
},
plugins: [vuePlugin()],
});
module.exports = config;

View File

@@ -0,0 +1,19 @@
{
"permissions": {
"allow": [
"Bash(mvn:*)",
"Bash(del:*)",
"Bash(rm:*)",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller\\AuthController.java\")",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\resources\\static\\html\\login.html\")",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller\\AuthProxyController.java\")",
"Bash(java:*)",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\resources\\static\\html\\shopee-platform.html\")",
"Bash(mkdir:*)",
"Bash(\"taskkill\" \"/f\" \"/pid\" \"9804\")",
"Bash(dir:*)",
"Bash(find:*)"
],
"deny": []
}
}

View File

@@ -0,0 +1 @@
DEVICE-CD31378598644EAA8C0E8153A9D80959

Binary file not shown.

185
erp_client_sb/pom.xml Normal file
View File

@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/>
</parent>
<groupId>com.tashow.erp</groupId>
<artifactId>erp_client_sb</artifactId>
<version>2.4.7</version>
<name>erp_client_sb</name>
<description>erp客户端</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.release>${java.version}</maven.compiler.release>
<java.version>17</java.version>
<springboot.version>3.5.4</springboot.version>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Apache POI for Excel processing -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- 已移除 JavaFX/FxWeaver 相关依赖,保留为纯 Spring Boot -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.12.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-core -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>1.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-extension -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>1.0.3</version>
</dependency>
<!-- JavaFX 相关依赖已移除 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.36</version>
</dependency>
<!-- SQLite数据库支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.42.0.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>6.2.7.Final</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.23.0</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.python/jython-standalone -->
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.4</version>
</dependency>
<!-- JWT parsing for local RS256 verification -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>aliyun-repository</id>
<name>aliyun repository</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
<repository>
<id>jboss-repository</id>
<name>jboss repository</name>
<url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url>
</repository>
</repositories>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>application*.yml</include>
<include>application*.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>application*.yml</exclude>
<exclude>application*.properties</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<mainClass>com.tashow.erp.ErpClientSbApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<!-- 移除 OpenJFX 打包插件,采用纯 Spring Boot 运行 -->
</plugins>
</build>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View File

@@ -0,0 +1,37 @@
package com.tashow.erp;
import com.tashow.erp.utils.ErrorReporter;
import com.tashow.erp.utils.ResourcePreloader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@Slf4j
@SpringBootApplication
public class ErpClientSbApplication {
public static void main(String[] args) {
// 纯 Spring Boot 启动,不再依赖 JavaFX
ConfigurableApplicationContext applicationContext = SpringApplication.run(ErpClientSbApplication.class, args);
try {
ErrorReporter errorReporter = applicationContext.getBean(ErrorReporter.class);
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
log.error("捕获到未处理异常: " + ex.getMessage(), ex);
errorReporter.reportSystemError("未捕获异常: " + thread.getName(), (Exception) ex);
});
log.info("全局异常处理器已设置");
} catch (Exception e) {
log.warn("未设置 ErrorReporter继续启动: {}", e.getMessage());
}
// 如需预加载资源,可按需保留
try {
ResourcePreloader.init();
ResourcePreloader.preloadErpDashboard();
ResourcePreloader.executePreloading();
} catch (Throwable t) {
log.warn("资源预加载失败: {}", t.getMessage());
}
}
}

View File

@@ -0,0 +1,85 @@
package com.tashow.erp.common;
import com.tashow.erp.utils.StringUtils;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* 字符集工具类
*
* @author ruoyi
*/
public class CharsetKit
{
/** ISO-8859-1 */
public static final String ISO_8859_1 = "ISO-8859-1";
/** UTF-8 */
public static final String UTF_8 = "UTF-8";
/** GBK */
public static final String GBK = "GBK";
/** ISO-8859-1 */
public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1);
/** UTF-8 */
public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8);
/** GBK */
public static final Charset CHARSET_GBK = Charset.forName(GBK);
/**
* 转换为Charset对象
*
* @param charset 字符集,为空则返回默认字符集
* @return Charset
*/
public static Charset charset(String charset)
{
return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset);
}
/**
* 转换字符串的字符集编码
*
* @param source 字符串
* @param srcCharset 源字符集默认ISO-8859-1
* @param destCharset 目标字符集默认UTF-8
* @return 转换后的字符集
*/
public static String convert(String source, String srcCharset, String destCharset)
{
return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));
}
/**
* 转换字符串的字符集编码
*
* @param source 字符串
* @param srcCharset 源字符集默认ISO-8859-1
* @param destCharset 目标字符集默认UTF-8
* @return 转换后的字符集
*/
public static String convert(String source, Charset srcCharset, Charset destCharset)
{
if (null == srcCharset)
{
srcCharset = StandardCharsets.ISO_8859_1;
}
if (null == destCharset)
{
destCharset = StandardCharsets.UTF_8;
}
if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset))
{
return source;
}
return new String(source.getBytes(srcCharset), destCharset);
}
/**
* @return 系统字符集编码
*/
public static String systemCharset()
{
return Charset.defaultCharset().name();
}
}

View File

@@ -0,0 +1,169 @@
package com.tashow.erp.common;
import java.util.Locale;
/**
* 通用常量信息
*
* @author ruoyi
*/
public class Constants
{
/**
* UTF-8 字符集
*/
public static final String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
public static final String GBK = "GBK";
/**
* 系统语言
*/
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
/**
* www主域
*/
public static final String WWW = "www.";
/**
* http请求
*/
public static final String HTTP = "http://";
/**
* https请求
*/
public static final String HTTPS = "https://";
/**
* 通用成功标识
*/
public static final String SUCCESS = "0";
/**
* 通用失败标识
*/
public static final String FAIL = "1";
/**
* 登录成功
*/
public static final String LOGIN_SUCCESS = "Success";
/**
* 注销
*/
public static final String LOGOUT = "Logout";
/**
* 注册
*/
public static final String REGISTER = "Register";
/**
* 登录失败
*/
public static final String LOGIN_FAIL = "Error";
/**
* 所有权限标识
*/
public static final String ALL_PERMISSION = "*:*:*";
/**
* 管理员角色权限标识
*/
public static final String SUPER_ADMIN = "admin";
/**
* 角色权限分隔符
*/
public static final String ROLE_DELIMETER = ",";
/**
* 权限标识分隔符
*/
public static final String PERMISSION_DELIMETER = ",";
/**
* 验证码有效期(分钟)
*/
public static final Integer CAPTCHA_EXPIRATION = 2;
/**
* 令牌
*/
public static final String TOKEN = "token";
/**
* 令牌前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 令牌前缀
*/
public static final String LOGIN_USER_KEY = "login_user_key";
/**
* 用户ID
*/
public static final String JWT_USERID = "userid";
/**
* 用户头像
*/
public static final String JWT_AVATAR = "avatar";
/**
* 创建时间
*/
public static final String JWT_CREATED = "created";
/**
* 用户权限
*/
public static final String JWT_AUTHORITIES = "authorities";
/**
* 资源映射路径 前缀
*/
public static final String RESOURCE_PREFIX = "/profile";
/**
* RMI 远程方法调用
*/
public static final String LOOKUP_RMI = "rmi:";
/**
* LDAP 远程方法调用
*/
public static final String LOOKUP_LDAP = "ldap:";
/**
* LDAPS 远程方法调用
*/
public static final String LOOKUP_LDAPS = "ldaps:";
/**
* 自动识别json对象白名单配置仅允许解析的包名范围越小越安全
*/
public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.ruoyi" };
/**
* 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
*/
public static final String[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" };
/**
* 定时任务违规的字符
*/
public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
"org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" };
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
package com.tashow.erp.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 数据库配置,确保数据目录存在
*/
@Slf4j
@Component
@Order(1) // 确保在其他组件之前运行
public class DatabaseConfig implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 确保data目录存在
Path dataDir = Paths.get("data");
if (!Files.exists(dataDir)) {
try {
Files.createDirectories(dataDir);
log.info("创建数据目录: {}", dataDir.toAbsolutePath());
} catch (Exception e) {
log.error("创建数据目录失败: {}", e.getMessage());
throw e;
}
} else {
log.info("数据目录已存在: {}", dataDir.toAbsolutePath());
}
}
}

View File

@@ -0,0 +1,50 @@
package com.tashow.erp.config;
import com.tashow.erp.utils.ErrorReporter;
import com.tashow.erp.utils.JsonData;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 错误上报切面 - 自动捕获所有Controller异常并上报同时检测错误返回值
*/
@Aspect
@Component
public class ErrorReportAspect {
@Autowired
private ErrorReporter errorReporter;
/**
* 拦截所有Controller方法自动上报异常和错误返回值
*/
@Around("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
public Object aroundController(ProceedingJoinPoint point) throws Throwable {
String methodName = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName();
try {
Object result = point.proceed();
// 检查返回值是否表示错误
if (result instanceof JsonData) {
JsonData jsonData = (JsonData) result;
// code != 0 表示失败根据JsonData注释0表示成功-1表示失败1表示处理中
if (jsonData.getCode() != null && jsonData.getCode() != 0) {
// 创建一个RuntimeException来包装错误信息
String errorMsg = jsonData.getMsg() != null ? jsonData.getMsg() : "未知错误";
Exception syntheticException = new RuntimeException("业务处理失败: " + errorMsg);
errorReporter.reportBusinessError(methodName + " 返回错误", syntheticException);
}
}
return result;
} catch (Exception e) {
// 自动上报未捕获的异常
errorReporter.reportBusinessError(methodName + " 抛出异常", e);
throw e;
}
}
}

View File

@@ -0,0 +1,18 @@
// package com.tashow.erp.config;
//
// 已移除 FxWeaver 相关配置(项目改为纯 Spring Boot
// 如需恢复 JavaFX 集成,请取消注释并恢复依赖。
//
// import net.rgielen.fxweaver.core.FxWeaver;
// import net.rgielen.fxweaver.spring.SpringFxWeaver;
// import org.springframework.context.ConfigurableApplicationContext;
// import org.springframework.context.annotation.Bean;
// import org.springframework.context.annotation.Configuration;
//
// @Configuration
// public class FxWeaverConfig {
// @Bean
// public FxWeaver fxWeaver(ConfigurableApplicationContext applicationContext) {
// return new SpringFxWeaver(applicationContext);
// }
// }

View File

@@ -0,0 +1,13 @@
package com.tashow.erp.config;
import com.tashow.erp.fx.controller.JavaBridge;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JavaBridgeConfig {
@Bean
public JavaBridge javaBridge() {
return new JavaBridge();
}
}

View File

@@ -0,0 +1,63 @@
// package com.tashow.erp.config;
//
// 已转为纯 Spring BootJavaFX 启动屏幕不再使用。如需恢复,可取消注释并补充 JavaFX 依赖。
//
// import javafx.application.Platform;
// import javafx.scene.Scene;
// import javafx.scene.control.ProgressBar;
// import javafx.scene.image.Image;
// import javafx.scene.image.ImageView;
// import javafx.scene.layout.StackPane;
// import javafx.scene.layout.VBox;
// import javafx.stage.Stage;
// import javafx.stage.StageStyle;
// import lombok.extern.slf4j.Slf4j;
// import org.springframework.stereotype.Component;
//
// /**
// * 自定义启动屏幕
// */
// @Slf4j
// @Component
// public class MySplashScreen {
// private static final String DEFAULT_IMAGE = "/static/image/splash_screen.png";
// private Stage splashStage;
// private ProgressBar progressBar;
//
// public void show() {
// Platform.runLater(() -> {
// try {
// splashStage = new Stage();
// splashStage.initStyle(StageStyle.UNDECORATED);
// Image splashImage = new Image(getClass().getResourceAsStream(DEFAULT_IMAGE));
// ImageView imageView = new ImageView(splashImage);
// ProgressBar progressBar = new ProgressBar();
// progressBar.setPrefWidth(splashImage.getWidth());
// progressBar.setMaxWidth(Double.MAX_VALUE);
// progressBar.setStyle("-fx-accent: #0078d4; -fx-background-color: transparent;");
// StackPane root = new StackPane();
// VBox progressContainer = new VBox();
// progressContainer.setAlignment(javafx.geometry.Pos.BOTTOM_CENTER);
// progressContainer.setSpacing(0);
// progressContainer.getChildren().add(progressBar);
// root.getChildren().addAll(imageView, progressContainer);
// Scene scene = new Scene(root);
// splashStage.setScene(scene);
// splashStage.setResizable(false);
// splashStage.centerOnScreen();
// splashStage.show();
// } catch (Exception e) {
// log.error("显示启动屏幕失败", e);
// }
// });
// }
//
// public void hide() {
// Platform.runLater(() -> {
// if (splashStage != null) {
// splashStage.hide();
// log.info("启动屏幕已隐藏");
// }
// });
// }
// }

View File

@@ -0,0 +1,19 @@
package com.tashow.erp.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@Component
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplateBuilder builder = new RestTemplateBuilder();
builder.connectTimeout(Duration.ofSeconds(5));
builder.readTimeout(Duration.ofSeconds(10));
return builder.build();
}
}

View File

@@ -0,0 +1,23 @@
package com.tashow.erp.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类
* 配置CORS以支持JavaFX WebView的网络请求
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8083", "http://127.0.0.1:8083", "file://")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

View File

@@ -0,0 +1,27 @@
package com.tashow.erp.config;
import com.tashow.erp.security.LocalJwtAuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LocalJwtAuthInterceptor localJwtAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localJwtAuthInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/login",
"/api/register",
"/api/verify",
"/api/check-username");
}
}

View File

@@ -0,0 +1,89 @@
package com.tashow.erp.controller;
import com.tashow.erp.repository.AmazonProductRepository;
import com.tashow.erp.service.IAmazonScrapingService;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/amazon")
public class AmazonController {
private static final Logger logger = LoggerUtil.getLogger(AmazonController.class);
@Autowired
private IAmazonScrapingService amazonScrapingService;
@Autowired
private AmazonProductRepository amazonProductRepository;
/**
* 批量获取亚马逊产品信息
*/
@PostMapping("/products/batch")
public JsonData batchGetProducts(@RequestBody Object request) {
@SuppressWarnings("unchecked")
Map<String, Object> requestMap = (Map<String, Object>) request;
List<String> asinList = (List<String>) requestMap.get("asinList");
String batchId = (String) requestMap.get("batchId");
return JsonData.buildSuccess(amazonScrapingService.batchGetProductInfo(asinList, batchId));
}
/**
* 获取最新产品数据
*/
@GetMapping("/products/latest")
public JsonData getLatestProducts() {
List<Map<String, Object>> products = amazonProductRepository.findLatestProducts()
.parallelStream()
.map(entity -> {
Map<String, Object> map = new HashMap<>();
map.put("asin", entity.getAsin());
map.put("title", entity.getTitle());
map.put("price", entity.getPrice());
map.put("imageUrl", entity.getImageUrl());
map.put("productUrl", entity.getProductUrl());
map.put("brand", entity.getBrand());
map.put("category", entity.getCategory());
map.put("rating", entity.getRating());
map.put("reviewCount", entity.getReviewCount());
map.put("availability", entity.getAvailability());
map.put("seller", entity.getSeller());
map.put("shipper", entity.getSeller());
return map;
})
.collect(Collectors.toList());
Map<String, Object> result = new HashMap<>();
result.put("products", products);
result.put("total", products.size());
return JsonData.buildSuccess(result);
}
/**
* 解析Excel文件获取ASIN列表
*/
@PostMapping("/import/asin")
public JsonData importAsinFromExcel(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return JsonData.buildError("上传文件为空");
}
try {
List<String> asinList = ExcelParseUtil.parseFirstColumn(file);
if (asinList.isEmpty()) {
return JsonData.buildError("未从文件中解析到ASIN数据");
}
Map<String, Object> result = new HashMap<>();
result.put("asinList", asinList);
result.put("total", asinList.size());
return JsonData.buildSuccess(result);
} catch (Exception e) {
logger.error("解析文件失败: {}", e.getMessage(), e);
return JsonData.buildError("解析失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,193 @@
package com.tashow.erp.controller;
import com.tashow.erp.entity.AuthTokenEntity;
import com.tashow.erp.entity.CacheDataEntity;
import com.tashow.erp.repository.AuthTokenRepository;
import com.tashow.erp.repository.CacheDataRepository;
import com.tashow.erp.service.IAuthService;
import com.tashow.erp.utils.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api")
public class AuthController {
@Autowired
private IAuthService authService;
@Autowired
private AuthTokenRepository authTokenRepository;
@Autowired
private CacheDataRepository cacheDataRepository;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, Object> loginData) {
String username = (String) loginData.get("username");
String password = (String) loginData.get("password");
if (username == null || password == null) {
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空"));
}
Map<String, Object> result = authService.login(username, password);
Object success = result.get("success");
Object tokenObj = result.get("token");
if (Boolean.TRUE.equals(success) && tokenObj instanceof String token && token != null && !token.isEmpty()) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
return ResponseEntity.ok().headers(headers).body(result);
}
return ResponseEntity.ok(result);
}
@PostMapping("/verify")
public ResponseEntity<?> verifyToken(@RequestBody Map<String, Object> data) {
String token = (String) data.get("token");
if (token == null) {
return ResponseEntity.ok(Map.of("code", 400, "message", "token不能为空"));
}
Map<String, Object> result = authService.verifyToken(token);
return ResponseEntity.ok(result);
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody Map<String, Object> registerData) {
String username = (String) registerData.get("username");
String password = (String) registerData.get("password");
if (username == null || password == null) {
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空"));
}
Map<String, Object> result = authService.register(username, password);
Object success2 = result.get("success");
Object tokenObj2 = result.get("token");
if (Boolean.TRUE.equals(success2) && tokenObj2 instanceof String token && token != null && !token.isEmpty()) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
return ResponseEntity.ok().headers(headers).body(result);
}
return ResponseEntity.ok(result);
}
@GetMapping("/check-username")
public ResponseEntity<?> checkUsername(@RequestParam String username) {
if (username == null || username.trim().isEmpty()) {
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名不能为空"));
}
boolean available = authService.checkUsername(username);
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "检查成功",
"data", available
));
}
/**
* 保存认证密钥
*/
@PostMapping("/auth/save")
public JsonData saveAuth(@RequestBody Map<String, Object> data) {
String serviceName = (String) data.get("serviceName");
String authKey = (String) data.get("authKey");
if (serviceName == null || authKey == null) return JsonData.buildError("serviceName和authKey不能为空");
AuthTokenEntity entity = authTokenRepository.findByServiceName(serviceName).orElse(new AuthTokenEntity());
entity.setServiceName(serviceName);
entity.setToken(authKey);
authTokenRepository.save(entity);
return JsonData.buildSuccess("认证信息保存成功");
}
@GetMapping("/auth/get")
public JsonData getAuth(@RequestParam String serviceName) {
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null));
}
/**
* 删除认证密钥
*/
@DeleteMapping("/auth/remove")
public JsonData removeAuth(@RequestParam String serviceName) {
authTokenRepository.findByServiceName(serviceName).ifPresent(authTokenRepository::delete);
return JsonData.buildSuccess("认证信息删除成功");
}
/**
* 保存缓存数据
*/
@PostMapping("/cache/save")
public JsonData saveCache(@RequestBody Map<String, Object> data) {
String key = (String) data.get("key");
String value = (String) data.get("value");
if (key == null || value == null) return JsonData.buildError("key和value不能为空");
CacheDataEntity entity = cacheDataRepository.findByCacheKey(key).orElse(new CacheDataEntity());
entity.setCacheKey(key);
entity.setCacheValue(value);
cacheDataRepository.save(entity);
return JsonData.buildSuccess("缓存数据保存成功");
}
/**
* 获取缓存数据
*/
@GetMapping("/cache/get")
public JsonData getCache(@RequestParam String key) {
return JsonData.buildSuccess(cacheDataRepository.findByCacheKey(key)
.map(CacheDataEntity::getCacheValue).orElse(null));
}
/**
* 删除缓存数据
*/
@DeleteMapping("/cache/remove")
public JsonData removeCache(@RequestParam String key) {
cacheDataRepository.findByCacheKey(key).ifPresent(cacheDataRepository::delete);
return JsonData.buildSuccess("缓存数据删除成功");
}
/**
* 删除缓存数据 - POST方式
*/
@PostMapping("/cache/delete")
public JsonData deleteCacheByPost(@RequestParam String key) {
if (key == null || key.trim().isEmpty()) {
return JsonData.buildError("key不能为空");
}
cacheDataRepository.deleteByCacheKey(key);
return JsonData.buildSuccess("缓存数据删除成功");
}
/**
* 会话引导检查SQLite中是否存在token
*/
@GetMapping("/session/bootstrap")
public ResponseEntity<?> sessionBootstrap() {
Optional<CacheDataEntity> tokenEntity = cacheDataRepository.findByCacheKey("token");
if (tokenEntity.isEmpty()) {
return ResponseEntity.status(401).body(Map.of("code", 401, "message", "无可用会话,请重新登录"));
}
String token = tokenEntity.get().getCacheValue();
if (token == null || token.isEmpty()) {
return ResponseEntity.status(401).body(Map.of("code", 401, "message", "无可用会话,请重新登录"));
}
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
return ResponseEntity.ok().headers(headers).body(Map.of("code", 200, "message", "会话已恢复"));
}
private String buildHttpOnlyCookie(String name, String value, int maxAgeSeconds) {
StringBuilder sb = new StringBuilder();
sb.append(name).append("=").append(value).append(";");
sb.append(" Path=/;");
sb.append(" HttpOnly;");
sb.append(" SameSite=Strict;");
if (maxAgeSeconds > 0) {
sb.append(" Max-Age=").append(maxAgeSeconds).append(";");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,116 @@
package com.tashow.erp.controller;
import com.tashow.erp.fx.controller.JavaBridge;
import com.tashow.erp.repository.BanmaOrderRepository;
import com.tashow.erp.service.IBanmaOrderService;
import com.tashow.erp.utils.ExcelExportUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/banma")
public class BanmaOrderController {
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderController.class);
@Autowired
IBanmaOrderService banmaOrderService;
@Autowired
BanmaOrderRepository banmaOrderRepository;
@Autowired
JavaBridge javaBridge;
@Autowired
RestTemplate restTemplate;
@GetMapping("/orders")
public ResponseEntity<Map<String, Object>> getOrders(
@RequestParam(required = false, name = "startDate") String startDate,
@RequestParam(required = false, name = "endDate") String endDate,
@RequestParam(defaultValue = "1", name = "page") int page,
@RequestParam(defaultValue = "10", name = "pageSize") int pageSize,
@RequestParam(required = false, name = "batchId") String batchId,
@RequestParam(required = false, name = "shopIds") String shopIds) {
List<String> shopIdList = shopIds != null ? java.util.Arrays.asList(shopIds.split(",")) : null;
Map<String, Object> result = banmaOrderService.getOrdersByPage(startDate, endDate, page, pageSize, batchId, shopIdList);
return ResponseEntity.ok(result);
}
/**
* 获取店铺列表
*/
@GetMapping("/shops")
public JsonData getShops() {
try {
Map<String, Object> response = banmaOrderService.getShops();
return JsonData.buildSuccess(response);
} catch (Exception e) {
logger.error("获取店铺列表失败: {}", e.getMessage(), e);
return JsonData.buildError("获取店铺列表失败: " + e.getMessage());
}
}
/**
* 刷新斑马认证Token
*/
@PostMapping("/refresh-token")
public JsonData refreshToken(){
try {
banmaOrderService.refreshToken();
return JsonData.buildSuccess("Token刷新成功");
} catch (Exception e) {
logger.error("刷新Token失败: {}", e.getMessage(), e);
return JsonData.buildError("Token刷新失败: " + e.getMessage());
}
}
/**
* 获取最新订单数据
*/
@GetMapping("/orders/latest")
public JsonData getLatestOrders() {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
List<Map<String, Object>> orders = banmaOrderRepository.findLatestOrders()
.parallelStream()
.map(entity -> {
try {
@SuppressWarnings("unchecked")
Map<String, Object> data = mapper.readValue(entity.getOrderData(), Map.class);
return data;
} catch (Exception e) {
return new HashMap<String, Object>();
}
})
.filter(order -> !order.isEmpty())
.toList();
return JsonData.buildSuccess(Map.of("orders", orders, "total", orders.size()));
}
/**
* JavaFX专用导出并保存Excel文件到桌面
*/
@PostMapping("/export-and-save")
public JsonData exportAndSave(@RequestBody Map<String, Object> body) {
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> orders = (List<Map<String, Object>>) body.get("orders");
String[] headers = {"下单时间", "商品图片", "商品名称", "乐天订单号", "下单距今时间", "乐天订单金额/日元",
"购买数量", "税费/日元", "服务商回款抽点rmb", "商品番号", "1688采购订单号",
"采购金额/rmb", "国际运费/rmb", "国内物流公司", "国内物流单号", "日本物流单号", "地址状态"};
byte[] excelData = ExcelExportUtil.createExcelWithImages("斑马订单数据", headers, orders, 1, "productImage");
if (excelData.length == 0) return JsonData.buildError("生成Excel文件失败");
String fileName = String.format("斑马订单数据_%s.xlsx", java.time.LocalDate.now().toString());
String savedPath = javaBridge.saveExcelFileToDesktop(excelData, fileName);
return savedPath != null
? JsonData.buildSuccess(Map.of("filePath", savedPath, "fileName", fileName))
: JsonData.buildError("保存文件失败,请检查权限");
} catch (Exception e) {
logger.error("导出并保存斑马订单Excel失败: {}", e.getMessage(), e);
return JsonData.buildError("导出并保存Excel失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,30 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 配置信息控制器
*/
@RestController
@RequestMapping("/api/config")
public class ConfigController {
@Value("${api.server.base-url}")
private String serverBaseUrl;
/**
* 获取服务器配置
*/
@GetMapping("/server")
public Map<String, Object> getServerConfig() {
return Map.of(
"baseUrl", serverBaseUrl,
"sseUrl", serverBaseUrl + "/monitor/account/events"
);
}
}

View File

@@ -0,0 +1,66 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import com.tashow.erp.utils.ApiForwarder;
import com.tashow.erp.utils.DeviceUtils;
import java.util.Map;
import java.util.HashMap;
/**
* 设备管理代理控制器
* 简化职责:透传请求到后端服务
*/
@RestController
public class DeviceProxyController {
@Autowired
private RestTemplate restTemplate;
@Value("${api.server.base-url}")
private String serverBaseUrl;
/**
* 注册设备
*/
@Autowired
private ApiForwarder apiForwarder;
@PostMapping("/api/device/register")
public ResponseEntity<?> deviceRegister(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
Map<String, Object> deviceData = new HashMap<>(body);
deviceData.put("deviceId", DeviceUtils.generateDeviceId());
return apiForwarder.post("/monitor/device/register", deviceData, auth);
}
@PostMapping("/api/device/remove")
public ResponseEntity<?> deviceRemove(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
return apiForwarder.post("/monitor/device/remove", body, auth);
}
/**
* 设备心跳
*/
@PostMapping("/api/device/heartbeat")
public ResponseEntity<?> deviceHeartbeat(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
return apiForwarder.post("/monitor/device/heartbeat", body, auth);
}
@GetMapping("/api/device/quota")
public ResponseEntity<?> deviceQuota(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) {
return apiForwarder.get("/monitor/device/quota?username=" + username, auth);
}
@GetMapping("/api/device/list")
public ResponseEntity<?> deviceList(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) {
return apiForwarder.get("/monitor/device/list?username=" + username, auth);
}
}

View File

@@ -0,0 +1,25 @@
package com.tashow.erp.controller;
import com.tashow.erp.service.IGenmaiService;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/genmai")
public class GenmaiController {
private static final Logger logger = LoggerUtil.getLogger(GenmaiController.class);
@Autowired
private IGenmaiService genmaiService;
/**
* 打开跟卖精灵网页
*/
@PostMapping("/open")
public void openGenmaiWebsite() {
genmaiService.openGenmaiWebsite();
}
}

View File

@@ -0,0 +1,18 @@
package com.tashow.erp.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "redirect:/html/erp-dashboard.html";
}
@GetMapping("/erp")
public String erp() {
return "redirect:/html/erp-dashboard.html";
}
}

View File

@@ -0,0 +1,116 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* 代理控制器用于解决CORS跨域问题
*/
@RestController
@RequestMapping("/api/proxy")
public class ProxyController {
@Autowired
private RestTemplate restTemplate;
/**
* 代理获取图片
* @param requestBody 包含图片URL的请求体
* @return 图片字节数组
*/
@PostMapping("/image")
public ResponseEntity<byte[]> proxyImage(@RequestBody Map<String, String> requestBody) {
String imageUrl = requestBody.get("imageUrl");
if (imageUrl == null || imageUrl.isEmpty()) {
return ResponseEntity.badRequest().build();
}
try {
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
HttpEntity<String> entity = new HttpEntity<>(headers);
// 发送请求获取图片
ResponseEntity<byte[]> response = restTemplate.exchange(
imageUrl,
HttpMethod.GET,
entity,
byte[].class
);
// 设置响应头
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 通过URL参数代理获取图片为JavaFX WebView优化
* @param imageUrl 图片URL
* @return 图片字节数组
*/
@GetMapping("/image-url")
public ResponseEntity<byte[]> proxyImageByUrl(@RequestParam("url") String imageUrl) {
if (imageUrl == null || imageUrl.isEmpty()) {
System.err.println("图片代理请求失败: 图片URL为空");
return ResponseEntity.badRequest().build();
}
System.out.println("代理图片请求: " + imageUrl);
try {
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
headers.set("Referer", "https://item.rakuten.co.jp/");
HttpEntity<String> entity = new HttpEntity<>(headers);
// 发送请求获取图片
ResponseEntity<byte[]> response = restTemplate.exchange(
imageUrl,
HttpMethod.GET,
entity,
byte[].class
);
System.out.println("图片代理成功,响应大小: " + (response.getBody() != null ? response.getBody().length : 0) + " bytes");
// 设置响应头支持缓存以提升JavaFX WebView性能
HttpHeaders responseHeaders = new HttpHeaders();
// 尝试从原始响应中获取Content-Type
String contentType = response.getHeaders().getFirst("Content-Type");
if (contentType != null && contentType.startsWith("image/")) {
responseHeaders.setContentType(MediaType.parseMediaType(contentType));
} else {
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
}
// 设置缓存头以提升性能
responseHeaders.setCacheControl("max-age=3600");
responseHeaders.set("Access-Control-Allow-Origin", "*");
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {
System.err.println("图片代理失败: " + imageUrl + " - " + e.getMessage());
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

View File

@@ -0,0 +1,163 @@
package com.tashow.erp.controller;
import com.tashow.erp.model.RakutenProduct;
import com.tashow.erp.model.SearchResult;
import com.tashow.erp.repository.RakutenProductRepository;
import com.tashow.erp.service.Alibaba1688Service;
import com.tashow.erp.service.IRakutenCacheService;
import com.tashow.erp.service.RakutenScrapingService;
import com.tashow.erp.service.impl.Alibaba1688ServiceImpl;
import com.tashow.erp.utils.DataReportUtil;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.QiniuUtil;
import com.tashow.erp.fx.controller.JavaBridge;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.client.RestTemplate;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.stream.Collectors;
import java.util.Base64;
@RestController
@RequestMapping("/api/rakuten")
@Slf4j
public class RakutenController {
@Autowired
private RakutenScrapingService rakutenScrapingService;
@Autowired
private Alibaba1688Service alibaba1688Service;
@Autowired
private IRakutenCacheService rakutenCacheService;
@Autowired
private JavaBridge javaBridge;
@Autowired
private DataReportUtil dataReportUtil;
/**
* 获取乐天商品数据(支持单个店铺名或 Excel 文件上传)
*
* @param file 可选Excel 文件(首列为店铺名)
* @param shopName 可选,单个店铺名
* @param batchId 可选,批次号
* @return JsonData 响应
*/
@PostMapping(value = "/products")
public JsonData getProducts(@RequestParam(value = "file", required = false) MultipartFile file, @RequestParam(value = "shopName", required = false) String shopName, @RequestParam(value = "batchId", required = false) String batchId) {
try {
// 1. 获取店铺名集合(优先 shopName其次 Excel
List<String> shopNames = Optional.ofNullable(shopName).filter(s -> !s.trim().isEmpty()).map(s -> List.of(s.trim())).orElseGet(() -> file != null ? ExcelParseUtil.parseFirstColumn(file) : new ArrayList<>());
if (CollectionUtils.isEmpty(shopNames)) {
return JsonData.buildError("未从 Excel 中解析到店铺名,且 shopName 参数为空");
}
List<RakutenProduct> allProducts = new ArrayList<>();
List<String> skippedShops = new ArrayList<>();
// 2. 遍历店铺,优先缓存,缺失则爬取
for (String currentShopName : shopNames) {
if (rakutenCacheService.hasRecentData(currentShopName)) {
// 从缓存获取
List<RakutenProduct> cached = rakutenCacheService.getProductsByShopName(currentShopName).stream().filter(p -> currentShopName.equals(p.getOriginalShopName())).toList();
rakutenCacheService.updateSpecificProductsSessionId(cached, batchId);
allProducts.addAll(cached);
skippedShops.add(currentShopName);
log.info("使用缓存数据,店铺: {},数量: {}", currentShopName, cached.size());
} else {
// 爬取新数据
log.info("采集新数据: {}", currentShopName);
List<RakutenProduct> fresh = rakutenScrapingService.scrapeProductsWithSearch(currentShopName);
fresh.forEach(p -> p.setOriginalShopName(currentShopName));
allProducts.addAll(fresh);
log.info("采集完成: {}, 数量: {}", currentShopName, fresh.size());
}
}
// 3. 处理新采集的数据,存储并生成 sessionId
List<RakutenProduct> newProducts = allProducts.stream().filter(p -> !skippedShops.contains(p.getOriginalShopName())).toList();
if (!newProducts.isEmpty()) {
// 使用已有 sessionId 保存
rakutenCacheService.saveProductsWithSessionId(newProducts, batchId);
}
// 4. 上报缓存数据使用情况
int cachedCount = allProducts.size() - newProducts.size();
if (cachedCount > 0) {
dataReportUtil.reportDataCollection("RAKUTEN_CACHE", cachedCount, "0");
}
// 5. 如果是单店铺查询,只返回该店铺的商品
List<RakutenProduct> finalProducts = (shopName != null && !shopName.trim().isEmpty()) ? allProducts.stream().filter(p -> shopName.trim().equals(p.getOriginalShopName())).toList() : allProducts;
return JsonData.buildSuccess(Map.of("products", finalProducts, "total", finalProducts.size(), "sessionId", batchId, "skippedShops", skippedShops, "newProductsCount", newProducts.size()));
} catch (Exception e) {
log.error("获取乐天商品失败", e);
return JsonData.buildError("获取乐天商品失败: " + e.getMessage());
}
}
/**
* 1688识图搜索API - 自动保存1688搜索结果
*/
@PostMapping("/search1688")
public JsonData search1688(@RequestBody Map<String, Object> params) {
String imageUrl = (String) params.get("imageUrl");
String sessionId = (String) params.get("sessionId");
try {
SearchResult result = alibaba1688Service.get1688Detail(imageUrl);
rakutenScrapingService.update1688DataByImageUrl(result, sessionId, imageUrl);
return JsonData.buildSuccess(result);
} catch (Exception e) {
log.error("1688识图搜索失败", e);
return JsonData.buildError("搜索失败: " + e.getMessage());
}
}
@GetMapping("/products/latest")
public JsonData getLatestProducts() {
try {
List<Map<String, Object>> products = rakutenScrapingService.getLatestProductsForDisplay();
return JsonData.buildSuccess(Map.of("products", products, "total", products.size()));
} catch (Exception e) {
e.printStackTrace();
log.info("获取最新商品数据失败", e);
return JsonData.buildError("获取最新数据失败: " + e.getMessage());
}
}
@PostMapping("/export-and-save")
public JsonData exportAndSave(@RequestBody Map<String, Object> body) {
try {
@SuppressWarnings("unchecked") List<Map<String, Object>> products = (List<Map<String, Object>>) body.get("products");
if (CollectionUtils.isEmpty(products)) return JsonData.buildError("没有可导出的数据");
boolean skipImages = Optional.ofNullable((Boolean) body.get("skipImages")).orElse(false);
String fileName = Optional.ofNullable((String) body.get("fileName")).filter(name -> !name.trim().isEmpty()).orElse("乐天商品数据_" + java.time.LocalDate.now() + ".xlsx");
String[] headers = {"店铺名", "商品图片", "商品链接", "排名", "商品标题", "价格", "1688识图链接", "1688价格", "1688重量"};
byte[] excelData = com.tashow.erp.utils.ExcelExportUtil.createExcelWithImages("乐天商品数据", headers, products, skipImages ? -1 : 1, skipImages ? null : "imgUrl");
if (excelData == null || excelData.length == 0) return JsonData.buildError("生成Excel失败");
String savedPath = javaBridge.saveExcelFileToDesktop(excelData, fileName);
if (savedPath == null) return JsonData.buildError("保存文件失败");
log.info("导出Excel: {}, 记录数: {}", fileName, products.size());
return JsonData.buildSuccess(Map.of("filePath", savedPath, "fileName", fileName, "recordCount", products.size(), "hasImages", !skipImages));
} catch (Exception e) {
log.error("导出Excel失败", e);
return JsonData.buildError("导出Excel失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,603 @@
package com.tashow.erp.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.entity.UpdateStatusEntity;
import com.tashow.erp.repository.UpdateStatusRepository;
import com.tashow.erp.service.IAuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* Web版本更新控制器
*
* @author Claude
*/
@RestController
@RequestMapping("/api/update")
public class UpdateController {
@Value("${project.version:2.3.6}")
private String currentVersion;
@Value("${project.build.time:}")
private String buildTime;
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private IAuthService authService;
@Autowired
private UpdateStatusRepository updateStatusRepository;
// 下载进度跟踪
private volatile int downloadProgress = 0;
private volatile long downloadedBytes = 0;
private volatile long totalBytes = 0;
private volatile String downloadStatus = "ready"; // ready, downloading, completed, failed, cancelled
private volatile String downloadSpeed = "0 KB/s";
private volatile long downloadStartTime = 0;
private volatile boolean downloadCancelled = false;
/**
* 获取当前版本号
*
* @return 当前版本号
*/
@GetMapping("/version")
public Map<String, Object> getVersion() {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("currentVersion", currentVersion);
result.put("buildTime", buildTime);
return result;
}
/**
* 检查版本更新
*
* @return 版本更新信息
*/
@GetMapping("/check")
public Map<String, Object> checkUpdate() {
Map<String, Object> result = new HashMap<>();
try {
String response = authService.checkVersion(currentVersion);
// 解析JSON响应
JsonNode responseNode = objectMapper.readTree(response);
result.put("success", true);
result.put("currentVersion", currentVersion);
// 检查响应格式并提取数据
JsonNode dataNode = null;
dataNode = responseNode.get("data");
// 直接使用服务器返回的needUpdate字段
boolean needUpdate = dataNode.has("needUpdate") &&
dataNode.get("needUpdate").asBoolean();
// 添加跳过版本信息,让前端自己判断
String skippedVersion = updateStatusRepository.findByKeyName("skippedUpdateVersion")
.map(entity -> entity.getValueData()).orElse(null);
result.put("skippedVersion", skippedVersion);
result.put("needUpdate", needUpdate);
// 获取最新版本号
if (dataNode.has("latestVersion")) {
result.put("latestVersion", dataNode.get("latestVersion").asText());
}
if (needUpdate) {
if (dataNode.has("downloadUrl")) {
String downloadUrl = dataNode.get("downloadUrl").asText();
result.put("downloadUrl", downloadUrl);
saveUpdateInfo("downloadUrl", downloadUrl);
}
if (dataNode.has("updateTime")) {
// 转换时间戳为可读格式
long updateTime = dataNode.get("updateTime").asLong();
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String releaseDate = sdf.format(new java.util.Date(updateTime));
result.put("releaseDate", releaseDate);
saveUpdateInfo("releaseDate", releaseDate);
}
if (dataNode.has("updateNotes")) {
String updateNotes = dataNode.get("updateNotes").asText();
result.put("updateNotes", updateNotes);
saveUpdateInfo("updateNotes", updateNotes);
}
if (dataNode.has("fileSize")) {
String fileSize = dataNode.get("fileSize").asText();
result.put("fileSize", fileSize);
saveUpdateInfo("fileSize", fileSize);
}
if (dataNode.has("latestVersion")) {
String latestVersion = dataNode.get("latestVersion").asText();
saveUpdateInfo("latestVersion", latestVersion);
}
} else {
// 如果不需要更新,清理之前保存的更新信息
clearUpdateInfo();
}
// 从SQLite读取之前保存的更新信息
result.putAll(getStoredUpdateInfo());
} catch (Exception e) {
result.put("success", false);
result.put("message", "版本检查失败:" + e.getMessage());
authService.reportError("UPDATE_CHECK_ERROR", "Web版本检查失败", e);
}
return result;
}
/**
* 保存更新信息到SQLite
*/
private void saveUpdateInfo(String key, String value) {
try {
UpdateStatusEntity entity = updateStatusRepository.findByKeyName(key)
.orElse(new UpdateStatusEntity());
entity.setKeyName(key);
entity.setValueData(value);
updateStatusRepository.save(entity);
} catch (Exception e) {
System.err.println("保存更新信息失败: " + key + " = " + value + ", 错误: " + e.getMessage());
}
}
/**
* 从SQLite获取存储的更新信息
*/
private Map<String, Object> getStoredUpdateInfo() {
Map<String, Object> info = new HashMap<>();
try {
String[] keys = {"downloadUrl", "releaseDate", "updateNotes", "fileSize", "latestVersion"};
for (String key : keys) {
updateStatusRepository.findByKeyName(key).ifPresent(entity ->
info.put(key, entity.getValueData())
);
}
} catch (Exception e) {
System.err.println("读取更新信息失败: " + e.getMessage());
}
return info;
}
/**
* 清理更新信息
*/
private void clearUpdateInfo() {
try {
String[] keys = {"downloadUrl", "releaseDate", "updateNotes", "fileSize", "latestVersion"};
for (String key : keys) {
updateStatusRepository.findByKeyName(key).ifPresent(entity ->
updateStatusRepository.delete(entity)
);
}
} catch (Exception e) {
System.err.println("清理更新信息失败: " + e.getMessage());
}
}
/**
* 获取下载进度
*
* @return 下载进度信息
*/
@GetMapping("/progress")
public Map<String, Object> getDownloadProgress() {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("progress", downloadProgress);
result.put("status", downloadStatus);
result.put("downloadedBytes", downloadedBytes);
result.put("totalBytes", totalBytes);
result.put("downloadSpeed", downloadSpeed);
result.put("downloadedMB", String.format("%.1f", downloadedBytes / 1024.0 / 1024.0));
result.put("totalMB", String.format("%.1f", totalBytes / 1024.0 / 1024.0));
return result;
}
/**
* 重置下载状态
*
* @return 重置结果
*/
@PostMapping("/reset")
public Map<String, Object> resetDownloadStatus() {
Map<String, Object> result = new HashMap<>();
downloadProgress = 0;
downloadedBytes = 0;
totalBytes = 0;
downloadStatus = "ready";
downloadSpeed = "0 KB/s";
downloadStartTime = 0;
downloadCancelled = false;
tempUpdateFilePath = null;
result.put("success", true);
result.put("message", "下载状态已重置");
return result;
}
/**
* 取消下载
*
* @return 取消结果
*/
@PostMapping("/cancel")
public Map<String, Object> cancelDownload() {
Map<String, Object> result = new HashMap<>();
if ("downloading".equals(downloadStatus)) {
downloadCancelled = true;
downloadStatus = "cancelled";
downloadSpeed = "已取消";
result.put("success", true);
result.put("message", "下载已取消");
} else if ("completed".equals(downloadStatus)) {
// 下载完成时点击取消,不删除文件,只是标记为稍后更新
result.put("success", true);
result.put("message", "已设置为稍后更新,文件保留");
System.out.println("用户选择稍后更新,文件路径: " + tempUpdateFilePath);
} else {
result.put("success", false);
result.put("message", "无效的操作状态");
}
return result;
}
/**
* 完全清除更新文件和状态
*
* @return 清除结果
*/
@PostMapping("/clear")
public Map<String, Object> clearUpdateFiles() {
Map<String, Object> result = new HashMap<>();
if (tempUpdateFilePath != null) {
try {
java.io.File tempFile = new java.io.File(tempUpdateFilePath);
if (tempFile.exists()) {
tempFile.delete();
System.out.println("已删除更新文件: " + tempUpdateFilePath);
}
} catch (Exception e) {
System.err.println("删除临时文件失败: " + e.getMessage());
}
}
// 重置状态
downloadProgress = 0;
downloadedBytes = 0;
totalBytes = 0;
downloadStatus = "ready";
downloadSpeed = "0 KB/s";
downloadCancelled = false;
tempUpdateFilePath = null;
result.put("success", true);
result.put("message", "更新文件和状态已清除");
return result;
}
/**
* 验证更新文件是否存在
*
* @return 验证结果
*/
@GetMapping("/verify-file")
public Map<String, Object> verifyUpdateFile() {
Map<String, Object> result = new HashMap<>();
try {
boolean fileExists = false;
String filePath = "";
if (tempUpdateFilePath != null) {
java.io.File updateFile = new java.io.File(tempUpdateFilePath);
fileExists = updateFile.exists();
filePath = tempUpdateFilePath;
if (fileExists) {
result.put("fileSize", updateFile.length());
result.put("lastModified", new java.util.Date(updateFile.lastModified()));
}
}
result.put("success", true);
result.put("fileExists", fileExists);
result.put("filePath", filePath);
result.put("downloadStatus", downloadStatus);
System.out.println("验证更新文件: " + filePath + ", 存在: " + fileExists);
} catch (Exception e) {
result.put("success", false);
result.put("message", "验证文件失败:" + e.getMessage());
result.put("fileExists", false);
}
return result;
}
/**
* 用户确认后执行安装
*
* @return 安装结果
*/
@PostMapping("/install")
public Map<String, Object> installUpdate() {
Map<String, Object> result = new HashMap<>();
if (tempUpdateFilePath == null || !new java.io.File(tempUpdateFilePath).exists()) {
result.put("success", false);
result.put("message", "更新文件不存在,请重新下载");
return result;
}
try {
result.put("success", true);
result.put("message", "开始安装更新...");
// 异步执行安装,避免阻塞响应
new Thread(() -> {
String updateScript = createUpdateScript(null, tempUpdateFilePath);
if (updateScript != null) {
executeUpdateAndExit(updateScript);
}
}).start();
} catch (Exception e) {
result.put("success", false);
result.put("message", "启动安装失败:" + e.getMessage());
}
return result;
}
/**
* 自动更新:下载、替换、重启
*
* @return 更新结果
*/
@PostMapping("/auto-update")
public Map<String, Object> autoUpdate(@RequestBody Map<String, String> request) {
Map<String, Object> result = new HashMap<>();
try {
String downloadUrl = request.get("downloadUrl");
result.put("success", true);
result.put("message", "开始自动更新...");
result.put("downloadUrl", downloadUrl);
String finalDownloadUrl = downloadUrl;
new Thread(() -> {
performUpdate(finalDownloadUrl);
}).start();
} catch (Exception e) {
result.put("success", false);
result.put("message", "启动更新失败:" + e.getMessage());
}
return result;
}
/**
* 执行更新过程
*/
private void performUpdate(String downloadUrl) {
String tempUpdateFile = downloadUpdate(downloadUrl);
if (tempUpdateFile == null) {
downloadStatus = "failed";
return;
}
// 下载完成后不自动重启,等待用户确认
downloadStatus = "completed";
System.out.println("下载完成,等待用户确认安装...");
// 将下载文件路径保存,供后续安装使用
this.tempUpdateFilePath = tempUpdateFile;
}
private String tempUpdateFilePath = null;
/**
* 下载更新文件
*/
private String downloadUpdate(String downloadUrl) {
try {
downloadStatus = "downloading";
downloadProgress = 0;
downloadedBytes = 0;
downloadStartTime = System.currentTimeMillis();
URL url = new URL(downloadUrl);
URLConnection connection = url.openConnection();
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
connection.setConnectTimeout(10000);
connection.setReadTimeout(30000);
// 获取文件大小
totalBytes = connection.getContentLength();
if (totalBytes <= 0) {
totalBytes = 70 * 1024 * 1024; // 默认70MB
}
String tempDir = System.getProperty("java.io.tmpdir");
String updateFileName = "erp_update_" + System.currentTimeMillis() + ".exe";
String tempUpdatePath = Paths.get(tempDir, updateFileName).toString();
System.out.println("开始下载更新文件,大小: " + (totalBytes / 1024 / 1024) + "MB");
try (InputStream inputStream = connection.getInputStream();
FileOutputStream outputStream = new FileOutputStream(tempUpdatePath)) {
byte[] buffer = new byte[8192];
int bytesRead;
long lastSpeedUpdate = System.currentTimeMillis();
long lastDownloadedBytes = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
if (downloadCancelled) {
System.out.println("下载已取消,停止下载进程");
downloadStatus = "cancelled";
downloadSpeed = "已取消";
outputStream.close();
try {
java.io.File partialFile = new java.io.File(tempUpdatePath);
if (partialFile.exists()) {
partialFile.delete();
}
} catch (Exception e) {
System.err.println("删除部分下载文件失败: " + e.getMessage());
}
return null;
}
outputStream.write(buffer, 0, bytesRead);
downloadedBytes += bytesRead;
// 计算下载进度
downloadProgress = (int) ((downloadedBytes * 100) / totalBytes);
// 每秒计算一次下载速度
long currentTime = System.currentTimeMillis();
if (currentTime - lastSpeedUpdate >= 1000) {
long speedBytes = downloadedBytes - lastDownloadedBytes;
double speedKB = speedBytes / 1024.0;
if (speedKB >= 1024) {
downloadSpeed = String.format("%.1f MB/s", speedKB / 1024.0);
} else {
downloadSpeed = String.format("%.1f KB/s", speedKB);
}
lastSpeedUpdate = currentTime;
lastDownloadedBytes = downloadedBytes;
System.out.println(String.format("下载进度: %d%% (%d/%d MB) 速度: %s",
downloadProgress,
downloadedBytes / 1024 / 1024,
totalBytes / 1024 / 1024,
downloadSpeed));
}
// 防止阻塞UI线程
if (downloadedBytes % (1024 * 1024) == 0) { // 每MB休息一下
Thread.sleep(10);
}
}
downloadStatus = "completed";
downloadProgress = 100;
downloadSpeed = "完成";
System.out.println("下载完成: " + (downloadedBytes / 1024 / 1024) + "MB");
}
return tempUpdatePath;
} catch (Exception e) {
downloadStatus = "failed";
downloadSpeed = "失败";
System.err.println("下载失败: " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
* 创建更新脚本
*/
private String createUpdateScript(String currentExePath, String tempUpdateFile) {
try {
String tempDir = System.getProperty("java.io.tmpdir");
String scriptPath = Paths.get(tempDir, "update_erp.bat").toString();
File currentDir = new File(System.getProperty("user.dir"));
File targetExe = new File(currentDir, "erpClient.exe");
String targetPath = targetExe.getAbsolutePath();
StringBuilder script = new StringBuilder();
script.append("@echo off\r\n");
script.append("chcp 65001 > nul\r\n");
script.append("echo 正在等待程序完全退出...\r\n");
script.append("timeout /t 1 /nobreak > nul\r\n");
script.append("echo 开始更新程序文件...\r\n");
script.append("if exist \"").append(targetPath).append("\" (");
script.append(" move \"").append(targetPath).append("\" \"").append(targetPath).append(".backup\"");
script.append(" )\r\n");
script.append("move \"").append(tempUpdateFile).append("\" \"").append(targetPath).append("\"\r\n");
script.append("if exist \"").append(targetPath).append("\" (\r\n");
script.append(" echo 更新成功程序将在1秒后重新启动...\r\n");
script.append(" timeout /t 1 /nobreak > nul\r\n");
script.append(" start \"\" \"").append(targetPath).append("\"\r\n");
script.append(" if exist \"").append(targetPath).append(".backup\" del \"").append(targetPath).append(".backup\"\r\n");
script.append(") else (\r\n");
script.append(" echo 更新失败!正在恢复原版本...\r\n");
script.append(" if exist \"").append(targetPath).append(".backup\" (\r\n");
script.append(" move \"").append(targetPath).append(".backup\" \"").append(targetPath).append("\"\r\n");
script.append(" echo 已恢复原版本程序将在1秒后重新启动...\r\n");
script.append(" timeout /t 1 /nobreak > nul\r\n");
script.append(" start \"\" \"").append(targetPath).append("\"\r\n");
script.append(" )\r\n");
script.append(")\r\n");
script.append("echo 更新操作完成!\r\n");
script.append("timeout /t 1 /nobreak > nul\r\n");
script.append("(goto) 2>nul & del \"%~f0\" & exit\r\n");
Files.write(Paths.get(scriptPath), script.toString().getBytes("GBK"));
return scriptPath;
} catch (Exception e) {
return null;
}
}
/**
* 执行更新脚本并退出程序
*/
private void executeUpdateAndExit(String scriptPath) {
try {
System.out.println("下载完成!正在准备更新...");
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", scriptPath);
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
pb.redirectError(ProcessBuilder.Redirect.DISCARD);
pb.start();
System.out.println("更新程序已启动,当前程序即将退出...");
Thread.sleep(1000);
Runtime.getRuntime().halt(0);
} catch (Exception e) {
Runtime.getRuntime().halt(1);
}
}
/**
* 保存跳过的版本
*/
@PostMapping("/skip-version")
public Map<String, Object> saveSkippedVersion(@RequestBody Map<String, String> request) {
Map<String, Object> result = new HashMap<>();
try {
saveUpdateInfo("skippedUpdateVersion", request.get("version"));
result.put("success", true);
} catch (Exception e) {
result.put("success", false);
}
return result;
}
}

View File

@@ -0,0 +1,59 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 1688产品实体类
*/
@Entity
@Table(name = "alibaba_1688_products")
@Data
public class Alibaba1688ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_id", unique = true, nullable = false)
private String productId;
@Column(name = "title", length = 1000)
private String title;
@Column(name = "price")
private String price;
@Column(name = "image_url", length = 1000)
private String imageUrl;
@Column(name = "product_url", length = 1000)
private String productUrl;
@Column(name = "supplier")
private String supplier;
@Column(name = "category")
private String category;
@Column(name = "min_order")
private String minOrder;
@Column(name = "trade_assurance")
private String tradeAssurance;
@Column(name = "session_id")
private String sessionId;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,64 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* Amazon产品缓存实体类
*/
@Entity
@Table(name = "amazon_products")
@Data
public class AmazonProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String asin;
@Column(name = "title", length = 1000)
private String title;
@Column(name = "price")
private String price;
@Column(name = "image_url", length = 1000)
private String imageUrl;
@Column(name = "product_url", length = 1000)
private String productUrl;
@Column(name = "brand")
private String brand;
@Column(name = "category")
private String category;
@Column(name = "rating")
private String rating;
@Column(name = "review_count")
private String reviewCount;
@Column(name = "availability")
private String availability;
@Column(name = "seller")
private String seller;
@Column(name = "session_id")
private String sessionId;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,38 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 认证令牌实体类
*/
@Entity
@Table(name = "auth_tokens")
@Data
public class AuthTokenEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "service_name", unique = true, nullable = false)
private String serviceName;
@Column(name = "token", nullable = false)
private String token;
@Column(name = "expire_time")
private LocalDateTime expireTime;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,38 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 斑马订单实体类
*/
@Entity
@Table(name = "banma_orders")
@Data
public class BanmaOrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "tracking_number", unique = true, nullable = false)
private String trackingNumber;
@Column(name = "order_data", columnDefinition = "TEXT")
private String orderData;
@Column(name = "session_id")
private String sessionId;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,38 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 缓存数据实体类
*/
@Entity
@Table(name = "cache_data")
@Data
public class CacheDataEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "cache_key", unique = true, nullable = false)
private String cacheKey;
@Column(name = "cache_value", columnDefinition = "TEXT")
private String cacheValue;
@Column(name = "expire_time")
private LocalDateTime expireTime;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,86 @@
package com.tashow.erp.entity;
import com.fasterxml.jackson.annotation.JsonRawValue;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 乐天产品缓存实体类
*/
@Entity
@Table(name = "rakuten_products")
@Data
public class RakutenProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "original_shop_name")
private String originalShopName;
@Column(name = "shop_name")
private String shopName;
@Column(name = "product_url", length = 1000)
private String productUrl;
@Column(name = "img_url", length = 1000)
private String imgUrl;
@Column(name = "product_title", length = 500)
private String productTitle;
@Column(name = "product_name", length = 500)
private String productName;
@Column(name = "price")
private String price;
@Column(name = "ranking")
private String ranking;
@Column(name = "price_1688")
private String price1688;
@Column(name = "detail_url_1688", length = 1000)
private String detailUrl1688;
@Column(name = "image_1688_url", length = 1000)
private String image1688Url;
@Column(name = "map_recognition_link", length = 1000)
private String mapRecognitionLink; // 1688识图链接
@Column(name = "freight")
private Double freight; // 运费
@Column(name = "median")
private Double median; // 中位价格
@Column(name = "weight")
private String weight; // 重量
@Column(name = "sku_price_json", columnDefinition = "JSON")
private String skuPriceJson; // SKU价格JSON字符串
@Column(name = "session_id")
private String sessionId; // 用于标识一次导入会话
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,81 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
/**
* 更新状态实体
*/
@Entity
@Table(name = "update_status")
public class UpdateStatusEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "key_name", unique = true)
private String keyName;
@Column(name = "value_data", columnDefinition = "TEXT")
private String valueData;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getKeyName() {
return keyName;
}
public void setKeyName(String keyName) {
this.keyName = keyName;
}
public String getValueData() {
return valueData;
}
public void setValueData(String valueData) {
this.valueData = valueData;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,65 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 斑马订单缓存实体类
*/
@Entity
@Table(name = "zebra_orders")
@Data
public class ZebraOrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_id")
private String orderId;
@Column(name = "product_name", length = 500)
private String productName;
@Column(name = "quantity")
private Integer quantity;
@Column(name = "price_jpy")
private String priceJpy;
@Column(name = "price_cny")
private String priceCny;
@Column(name = "image_url", length = 1000)
private String imageUrl;
@Column(name = "customer_name")
private String customerName;
@Column(name = "order_status")
private String orderStatus;
@Column(name = "order_date")
private String orderDate;
@Column(name = "shipping_address", length = 1000)
private String shippingAddress;
@Column(name = "tracking_number")
private String trackingNumber;
@Column(name = "session_id")
private String sessionId;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,79 @@
package com.tashow.erp.fx.controller;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@Slf4j
public class JavaBridge {
/**
* 直接保存字节数组为Excel文件到桌面纯 Spring Boot 环境,无文件对话框)
*/
public String saveExcelFileToDesktop(byte[] data, String fileName) {
try {
if (data == null || data.length == 0) {
log.warn("文件数据为空,无法保存文件");
return null;
}
String userHome = System.getProperty("user.home");
File desktop = new File(userHome, "Desktop");
if (!desktop.exists()) {
// 回退到用户目录
desktop = new File(userHome);
}
File file = new File(desktop, fileName);
int counter = 1;
if (fileName != null && fileName.contains(".")) {
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
String extension = fileName.substring(fileName.lastIndexOf('.'));
while (file.exists()) {
file = new File(desktop, baseName + "_" + counter + extension);
counter++;
}
} else {
while (file.exists()) {
file = new File(desktop, fileName + "_" + counter);
counter++;
}
}
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(data);
fos.flush();
}
String filePath = file.getAbsolutePath();
log.info("Excel文件已保存: {}", filePath);
return filePath;
} catch (IOException e) {
log.error("保存Excel文件失败: {}", e.getMessage(), e);
return null;
}
}
/**
* 复制文本到系统剪贴板
*/
public boolean copyToClipboard(String text) {
try {
if (text == null || text.trim().isEmpty()) {
return false;
}
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
StringSelection selection = new StringSelection(text);
clipboard.setContents(selection, null);
return true;
} catch (Exception e) {
log.error("复制到剪贴板失败: {}", e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,43 @@
// package com.tashow.erp.fx.controller;
//
// 已转为纯 Spring Boot不再包含 JavaFX 控制器与视图逻辑。如需恢复 FX请取消注释并恢复依赖。
//
// import javafx.application.Platform;
// import javafx.fxml.FXML;
// import javafx.fxml.Initializable;
// import javafx.scene.layout.BorderPane;
// import javafx.scene.web.WebEngine;
// import javafx.scene.web.WebView;
// import lombok.extern.slf4j.Slf4j;
// import net.rgielen.fxweaver.core.FxmlView;
// import org.springframework.stereotype.Component;
// import netscape.javascript.JSObject;
//
// import java.net.URL;
// import java.util.ResourceBundle;
//
// @Slf4j
// @Component
// @FxmlView("/static/fxml/Main.fxml")
// public class MainCtrl implements Initializable {
// @FXML
// public BorderPane rootPane;
// @FXML
// public WebView webView;
// private WebEngine webEngine;
// @Override
// public void initialize(URL location, ResourceBundle resources) {
// if (Platform.isFxApplicationThread()) {
// initWebView();
// } else {
// Platform.runLater(this::initWebView);
// }
// }
//
// private void initWebView() {
// webEngine = webView.getEngine();
// webEngine.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...");
// webEngine.setJavaScriptEnabled(true);
// webEngine.load("http://localhost:8081/html/erp-dashboard.html");
// }
// }

View File

@@ -0,0 +1,11 @@
// package com.tashow.erp.fx.view;
//
// 已转为纯 Spring Boot移除 FX 视图。
// import javafx.scene.Parent;
// import net.rgielen.fxweaver.core.FxmlView;
// import org.springframework.stereotype.Component;
//
// @Component
// @FxmlView("/static/fxml/Main.fxml")
// public class MainView {
// }

View File

@@ -0,0 +1,25 @@
package com.tashow.erp.model;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 1688爬取风控监控数据表实体类
*/
@Data
public class ClientAlibaba1688Monitor {
private Long id;
private String clientId;
private String ipAddress;
private String eventType;
private Long eventTime;
private Long duration;
private LocalDateTime createTime;
private String remark;
}

View File

@@ -0,0 +1,23 @@
package com.tashow.erp.model;
import lombok.Data;
/**
* 监控数据类
*/
@Data
public class MonitoringData {
private String ipAddress;
private MonitorEventType eventType;
private long eventTime;
private long duration;
/**
* 风控事件类型
*/
public enum MonitorEventType {
MOBILE_FIRST_ACCESS, // 移动端首次访问
MOBILE_BLOCKED, // 移动端被风控
DESKTOP_BLOCKED // 电脑端被风控
}
}

Some files were not shown because too many files have changed in this diff Show More