Compare commits
11 Commits
c2e1617a99
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 358203b11d | |||
| 02858146b3 | |||
| bff057c99b | |||
| d29d4d69da | |||
| 937a84bb81 | |||
| f9d1848280 | |||
| dd23d9fe90 | |||
| 007799fb2a | |||
| cfb9096788 | |||
| cce281497b | |||
| 92ab782943 |
270
CLAUDE.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a **hybrid ERP system** consisting of:
|
||||||
|
1. **Backend**: RuoYi-Vue (Spring Boot 2.5.15 + MyBatis) - Java 17 management system
|
||||||
|
2. **Desktop Client**: Electron + Vue 3 + TypeScript application
|
||||||
|
3. **Embedded Spring Boot Service**: Java service that runs within the Electron app
|
||||||
|
|
||||||
|
The architecture uses a dual-service pattern where the Electron app communicates with both:
|
||||||
|
- Local embedded Spring Boot service (port 8081)
|
||||||
|
- Remote RuoYi admin backend (port 8085)
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\wox\erp\
|
||||||
|
├── electron-vue-template/ # Electron + Vue 3 desktop client
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── main/ # Electron main process (TypeScript)
|
||||||
|
│ │ └── renderer/ # Vue 3 renderer process
|
||||||
|
│ │ ├── api/ # API client modules
|
||||||
|
│ │ ├── components/ # Vue components
|
||||||
|
│ │ └── utils/ # Utility functions
|
||||||
|
│ ├── scripts/ # Build scripts
|
||||||
|
│ └── package.json
|
||||||
|
├── ruoyi-admin/ # Main Spring Boot application entry
|
||||||
|
├── ruoyi-system/ # System management module
|
||||||
|
├── ruoyi-framework/ # Framework core (Security, Redis, etc.)
|
||||||
|
├── ruoyi-common/ # Common utilities
|
||||||
|
├── ruoyi-generator/ # Code generator
|
||||||
|
├── ruoyi-quartz/ # Scheduled tasks
|
||||||
|
├── erp_client_sb/ # Embedded Spring Boot service for client
|
||||||
|
├── sql/ # Database migration scripts
|
||||||
|
└── pom.xml # Root Maven configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Backend (Spring Boot)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the project (from root)
|
||||||
|
mvn clean package
|
||||||
|
|
||||||
|
# Run the RuoYi admin backend
|
||||||
|
cd ruoyi-admin
|
||||||
|
mvn spring-boot:run
|
||||||
|
# Runs on http://localhost:8085
|
||||||
|
|
||||||
|
# Build without tests
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (Electron + Vue)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd electron-vue-template
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Development mode with hot reload
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for distribution
|
||||||
|
npm run build # Cross-platform
|
||||||
|
npm run build:win # Windows
|
||||||
|
npm run build:mac # macOS
|
||||||
|
npm run build:linux # Linux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Architecture Patterns
|
||||||
|
|
||||||
|
### 1. Dual-Backend Routing (http.ts)
|
||||||
|
|
||||||
|
The Electron client uses intelligent routing to determine which backend to call:
|
||||||
|
- Paths starting with `/monitor/`, `/system/`, `/tool/banma`, `/tool/genmai` → RuoYi backend (port 8085)
|
||||||
|
- All other paths → Embedded Spring Boot service (port 8081)
|
||||||
|
|
||||||
|
**Location**: `electron-vue-template/src/renderer/api/http.ts`
|
||||||
|
|
||||||
|
### 2. Account-Based Resource Isolation
|
||||||
|
|
||||||
|
User-specific resources (splash images, brand logos) are stored per account:
|
||||||
|
- Backend stores URLs in the `client_account` table (columns: `splash_image`, `brand_logo`)
|
||||||
|
- Files are uploaded to Qiniu Cloud (七牛云) configured in `application.yml`
|
||||||
|
- Each user sees only their own uploaded assets
|
||||||
|
|
||||||
|
**Key files**:
|
||||||
|
- Java entity: `ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccount.java`
|
||||||
|
- MyBatis mapper: `ruoyi-system/src/main/resources/mapper/system/ClientAccountMapper.xml`
|
||||||
|
- API endpoints: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java`
|
||||||
|
|
||||||
|
### 3. Event-Driven UI Updates
|
||||||
|
|
||||||
|
The Vue application uses custom browser events to propagate state changes between components:
|
||||||
|
- `window.dispatchEvent(new CustomEvent('brandLogoChanged'))` - notifies when brand logo changes
|
||||||
|
- `window.addEventListener('brandLogoChanged', handler)` - listens for changes in App.vue
|
||||||
|
|
||||||
|
This pattern ensures immediate UI updates after upload/delete operations without requiring page refreshes.
|
||||||
|
|
||||||
|
### 4. VIP Feature Gating
|
||||||
|
|
||||||
|
Certain features (e.g., custom splash images, brand logos) are gated by VIP status:
|
||||||
|
- Check `accountType` field in `ClientAccount` (values: `trial`, `paid`)
|
||||||
|
- Trial accounts show `TrialExpiredDialog` when attempting VIP features
|
||||||
|
- VIP validation happens in `SettingsDialog.vue` before allowing uploads
|
||||||
|
|
||||||
|
## Important Configuration
|
||||||
|
|
||||||
|
### Backend Configuration
|
||||||
|
|
||||||
|
**File**: `ruoyi-admin/src/main/resources/application.yml`
|
||||||
|
|
||||||
|
Key settings:
|
||||||
|
- Server port: `8085`
|
||||||
|
- Upload path: `ruoyi.profile: D:/ruoyi/uploadPath`
|
||||||
|
- Redis: `8.138.23.49:6379` (password: `123123`)
|
||||||
|
- Qiniu Cloud credentials for file storage
|
||||||
|
- Token expiration: 30 minutes
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
The system uses MySQL with MyBatis. When adding new fields:
|
||||||
|
1. Write SQL migration script in `sql/` directory
|
||||||
|
2. Update Java entity in `ruoyi-system/src/main/java/com/ruoyi/system/domain/`
|
||||||
|
3. Update MyBatis mapper XML in `ruoyi-system/src/main/resources/mapper/system/`
|
||||||
|
4. Include field in `<resultMap>`, `<sql id="select...">`, `<insert>`, and `<update>` sections
|
||||||
|
|
||||||
|
### Electron Main Process
|
||||||
|
|
||||||
|
**File**: `electron-vue-template/src/main/main.ts`
|
||||||
|
|
||||||
|
- Manages embedded Spring Boot process lifecycle
|
||||||
|
- Handles splash screen display
|
||||||
|
- Configures tray icon
|
||||||
|
- Manages auto-updates
|
||||||
|
- Uses app data directory: `app.getPath('userData')`
|
||||||
|
|
||||||
|
## Development Workflow (from .cursor/rules/guize.mdc)
|
||||||
|
|
||||||
|
When making code changes, follow this three-phase approach:
|
||||||
|
|
||||||
|
### Phase 1: Analyze Problem (【分析问题】)
|
||||||
|
- Understand user intent and ask clarifying questions
|
||||||
|
- Search all related code
|
||||||
|
- Identify root cause
|
||||||
|
- Look for code smells: duplication, poor naming, outdated patterns, inconsistent types
|
||||||
|
- Ask questions if multiple solutions exist
|
||||||
|
|
||||||
|
### Phase 2: Plan Solution (【制定方案】)
|
||||||
|
- List files to be created/modified/deleted
|
||||||
|
- Describe changes briefly for each file
|
||||||
|
- Eliminate code duplication through reuse/abstraction
|
||||||
|
- Ensure DRY principles and good architecture
|
||||||
|
- Ask questions if key decisions are unclear
|
||||||
|
|
||||||
|
### Phase 3: Execute (【执行方案】)
|
||||||
|
- Implement according to the approved plan
|
||||||
|
- Run type checking after modifications
|
||||||
|
- **DO NOT** commit code unless explicitly requested
|
||||||
|
- **DO NOT** start dev servers automatically
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Adding a New API Endpoint
|
||||||
|
|
||||||
|
1. **Backend** (Spring Boot):
|
||||||
|
```java
|
||||||
|
// In appropriate Controller (e.g., ClientAccountController.java)
|
||||||
|
@PostMapping("/your-endpoint")
|
||||||
|
public AjaxResult yourMethod(@RequestBody YourDTO dto) {
|
||||||
|
// Implementation
|
||||||
|
return AjaxResult.success(result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Frontend** (Vue/TypeScript):
|
||||||
|
```typescript
|
||||||
|
// In electron-vue-template/src/renderer/api/your-module.ts
|
||||||
|
export const yourApi = {
|
||||||
|
async yourMethod(data: YourType) {
|
||||||
|
return http.post<ResponseType>('/your-endpoint', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Component usage**:
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { yourApi } from '@/api/your-module'
|
||||||
|
|
||||||
|
const handleAction = async () => {
|
||||||
|
try {
|
||||||
|
const res = await yourApi.yourMethod(data)
|
||||||
|
// Handle success, update local state immediately
|
||||||
|
localState.value = res.data
|
||||||
|
// Dispatch event if other components need to know
|
||||||
|
window.dispatchEvent(new CustomEvent('yourEventName'))
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Upload Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Frontend
|
||||||
|
const handleUpload = async (file: File) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('username', currentUsername)
|
||||||
|
|
||||||
|
const res = await splashApi.uploadSomething(file, username)
|
||||||
|
if (res.url) {
|
||||||
|
localImageUrl.value = res.url // Update immediately
|
||||||
|
window.dispatchEvent(new CustomEvent('imageChanged'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Backend Controller
|
||||||
|
@PostMapping("/upload")
|
||||||
|
public AjaxResult upload(@RequestParam("file") MultipartFile file) {
|
||||||
|
String url = qiniuService.uploadFile(file);
|
||||||
|
// Save URL to database
|
||||||
|
return AjaxResult.success(url);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technology Stack Details
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Framework**: Spring Boot 2.5.15
|
||||||
|
- **Security**: Spring Security 5.7.12 + JWT
|
||||||
|
- **ORM**: MyBatis with PageHelper
|
||||||
|
- **Database**: MySQL
|
||||||
|
- **Cache**: Redis (Lettuce client)
|
||||||
|
- **File Storage**: Qiniu Cloud (七牛云)
|
||||||
|
- **API Docs**: Swagger 3.0.0
|
||||||
|
- **Build**: Maven
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Framework**: Vue 3.3.8 (Composition API with `<script setup>`)
|
||||||
|
- **Desktop**: Electron 32.1.2
|
||||||
|
- **Build**: Vite 4.5.0
|
||||||
|
- **UI Library**: Element Plus 2.11.3
|
||||||
|
- **Language**: TypeScript 5.2.2
|
||||||
|
- **Excel**: ExcelJS 4.4.0
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Currently, there is no explicit test framework configured. When adding tests:
|
||||||
|
- Backend: Use JUnit with Spring Boot Test
|
||||||
|
- Frontend: Consider Vitest (already compatible with Vite)
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Chinese Language**: All user-facing text should be in Chinese (simplified)
|
||||||
|
- **Code Style**: Follow existing patterns - keep code concise and avoid unnecessary abstractions
|
||||||
|
- **No Auto-commit**: Never commit changes unless explicitly requested by the user
|
||||||
|
- **Secrets**: Qiniu Cloud keys are in `application.yml` - never expose in client code
|
||||||
|
- **Token Management**: JWT tokens stored in Electron via `utils/token.ts`, sent in `Authorization` header
|
||||||
|
- **Image Proxy**: Custom protocol handler in Electron for loading images from backend
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<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);
|
|
||||||
```
|
|
||||||
7281
electron-vue-template/package-lock.json
generated
Normal file
@@ -2,7 +2,7 @@
|
|||||||
"name": "erpClient",
|
"name": "erpClient",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "A minimal Electron + Vue application",
|
"description": "A minimal Electron + Vue application",
|
||||||
"main": "main/main.js",
|
"main": "build/main/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/dev-server.js",
|
"dev": "node scripts/dev-server.js",
|
||||||
"build": "node scripts/build.js && electron-builder --dir",
|
"build": "node scripts/build.js && electron-builder --dir",
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.4.1",
|
"@vitejs/plugin-vue": "^4.4.1",
|
||||||
|
"binary-extensions": "^3.1.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"electron": "^32.1.2",
|
"electron": "^32.1.2",
|
||||||
@@ -32,5 +33,26 @@
|
|||||||
"element-plus": "^2.11.3",
|
"element-plus": "^2.11.3",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"vue": "^3.3.8"
|
"vue": "^3.3.8"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.tashow.erp",
|
||||||
|
"productName": "天骄智能电商",
|
||||||
|
"files": [
|
||||||
|
"build/**/*",
|
||||||
|
"node_modules/**/*",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "assets",
|
||||||
|
"output": "dist"
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "dir",
|
||||||
|
"arch": ["x64"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4417
electron-vue-template/pnpm-lock.yaml
generated
Normal file
|
Before Width: | Height: | Size: 913 B |
|
Before Width: | Height: | Size: 894 B |
|
Before Width: | Height: | Size: 870 B |
BIN
electron-vue-template/public/icon/vipExclusive.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 533 B After Width: | Height: | Size: 533 B |
BIN
electron-vue-template/public/image/excel-format-example.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
@@ -1,31 +1,78 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>正在启动...</title>
|
<title>正在启动...</title>
|
||||||
<style>
|
<style>
|
||||||
html, body { height: 100%; margin: 0; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
html, body { height: 100%; overflow: hidden; }
|
||||||
body {
|
body {
|
||||||
background: #fff; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
|
display: flex;
|
||||||
background-image: var(--splash-image, url('./image/splash_screen.png'));
|
flex-direction: column;
|
||||||
background-repeat: no-repeat;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
background-position: center;
|
}
|
||||||
background-size: cover;
|
.image {
|
||||||
|
flex: 1;
|
||||||
|
background-image: __SPLASH_IMAGE__;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.box {
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 30px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0,0,0,0.85);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(0,0,0,0.06);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background: linear-gradient(90deg, #1677ff 0%, #4096ff 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
animation: load 3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 4px 15px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
background: #f0f7ff;
|
||||||
|
}
|
||||||
|
@keyframes load {
|
||||||
|
to { width: 90%; }
|
||||||
}
|
}
|
||||||
.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>
|
</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: file: https:; style-src 'self' 'unsafe-inline';">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="image"></div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
<span class="text">正在启动</span>
|
||||||
<div class="progress"><div class="bar"></div></div>
|
<div class="progress"><div class="bar"></div></div>
|
||||||
|
<button class="btn" onclick="require('electron').ipcRenderer.send('quit-app')">退出</button>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ let springProcess: ChildProcess | null = null;
|
|||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let splashWindow: BrowserWindow | null = null;
|
let splashWindow: BrowserWindow | null = null;
|
||||||
let appOpened = false;
|
let appOpened = false;
|
||||||
|
let splashStartTime = 0; // 记录 splash 窗口显示时间
|
||||||
let downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
let downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
||||||
let isDownloading = false;
|
let isDownloading = false;
|
||||||
let downloadedFilePath: string | null = null;
|
let downloadedFilePath: string | null = null;
|
||||||
@@ -23,13 +24,22 @@ function openAppIfNotOpened() {
|
|||||||
!appOpened && setTimeout(openAppIfNotOpened, 50);
|
!appOpened && setTimeout(openAppIfNotOpened, 50);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appOpened = true;
|
appOpened = true;
|
||||||
isDev
|
const url = `http://localhost:${process.argv[2] || 8083}`;
|
||||||
? mainWindow.loadURL(`http://localhost:${process.argv[2] || 8083}`)
|
isDev
|
||||||
|
? mainWindow.loadURL(url)
|
||||||
: mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
: mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||||
|
|
||||||
mainWindow.webContents.once('did-finish-load', () => {
|
mainWindow.webContents.once('did-finish-load', () => {
|
||||||
|
if (splashWindow && !splashWindow.isDestroyed()) {
|
||||||
|
splashWindow.webContents.send('splash-complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算 splash 已显示的时间,确保至少显示 2 秒
|
||||||
|
const splashElapsed = Date.now() - splashStartTime;
|
||||||
|
const minSplashTime = 2000;
|
||||||
|
const remainingTime = Math.max(0, minSplashTime - splashElapsed);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const shouldMinimize = loadConfig().launchMinimized || false;
|
const shouldMinimize = loadConfig().launchMinimized || false;
|
||||||
@@ -37,16 +47,20 @@ function openAppIfNotOpened() {
|
|||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
if (isDev) mainWindow.webContents.openDevTools();
|
|
||||||
}
|
}
|
||||||
if (splashWindow && !splashWindow.isDestroyed()) {
|
|
||||||
splashWindow.close();
|
// 延迟关闭 splash
|
||||||
splashWindow = null;
|
setTimeout(() => {
|
||||||
}
|
if (splashWindow && !splashWindow.isDestroyed()) {
|
||||||
}, 100);
|
splashWindow.close();
|
||||||
|
splashWindow = null;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}, remainingTime + 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 通用资源路径获取函数
|
// 通用资源路径获取函数
|
||||||
function getResourcePath(devPath: string, prodPath: string, fallbackPath?: string): string {
|
function getResourcePath(devPath: string, prodPath: string, fallbackPath?: string): string {
|
||||||
if (isDev) return join(__dirname, devPath);
|
if (isDev) return join(__dirname, devPath);
|
||||||
@@ -76,6 +90,138 @@ const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/
|
|||||||
const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png');
|
const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png');
|
||||||
const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml');
|
const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml');
|
||||||
|
|
||||||
|
// 图片缓存目录
|
||||||
|
const getImageCacheDir = () => {
|
||||||
|
const cacheDir = join(app.getPath('userData'), 'image-cache');
|
||||||
|
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
||||||
|
return cacheDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载图片到本地
|
||||||
|
async function downloadImageToLocal(imageUrl: string, username: string, type: 'splash' | 'logo' | 'global_splash'): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const handleResponse = (res: http.IncomingMessage) => {
|
||||||
|
if (res.statusCode !== 200) return resolve();
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
const ext = imageUrl.match(/\.(jpg|jpeg|png|gif|webp)$/i)?.[1] || 'png';
|
||||||
|
const filename = type === 'global_splash' ? `global_splash.${ext}` : `${username}_${type}.${ext}`;
|
||||||
|
const filepath = join(getImageCacheDir(), filename);
|
||||||
|
writeFileSync(filepath, buffer);
|
||||||
|
console.log(`[图片缓存] 已保存: ${filename}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
res.on('error', () => resolve());
|
||||||
|
};
|
||||||
|
const req = imageUrl.startsWith('https') ? https.get(imageUrl, handleResponse) : http.get(imageUrl, handleResponse);
|
||||||
|
req.on('error', () => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载本地缓存图片
|
||||||
|
function loadCachedImage(username: string, type: 'splash' | 'logo' | 'global_splash'): string | null {
|
||||||
|
try {
|
||||||
|
const files = readdirSync(getImageCacheDir());
|
||||||
|
const prefix = type === 'global_splash' ? 'global_splash.' : `${username}_${type}.`;
|
||||||
|
const file = files.find(f => f.startsWith(prefix));
|
||||||
|
if (file) {
|
||||||
|
const buffer = readFileSync(join(getImageCacheDir(), file));
|
||||||
|
const ext = extname(file).slice(1);
|
||||||
|
const mime = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp' }[ext] || 'png';
|
||||||
|
return `url('data:image/${mime};base64,${buffer.toString('base64')}')`;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除本地缓存图片
|
||||||
|
function deleteCachedImage(username: string, type: 'splash' | 'logo' | 'global_splash'): void {
|
||||||
|
try {
|
||||||
|
const files = readdirSync(getImageCacheDir());
|
||||||
|
const prefix = type === 'global_splash' ? 'global_splash.' : `${username}_${type}.`;
|
||||||
|
const file = files.find(f => f.startsWith(prefix));
|
||||||
|
if (file) {
|
||||||
|
const filepath = join(getImageCacheDir(), file);
|
||||||
|
if (existsSync(filepath)) {
|
||||||
|
require('fs').unlinkSync(filepath);
|
||||||
|
console.log(`[图片缓存] 已删除: ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从服务器同步获取全局开屏图片URL(带超时)
|
||||||
|
async function fetchGlobalSplashImageUrl(): Promise<string | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const config = loadConfig();
|
||||||
|
const serverUrl = config.serverUrl || 'http://8.138.23.49:8085';
|
||||||
|
const url = `${serverUrl}/monitor/account/global-splash-image`;
|
||||||
|
|
||||||
|
const handleResponse = (res: http.IncomingMessage) => {
|
||||||
|
if (res.statusCode !== 200) return resolve(null);
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => data += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
resolve(json.code === 200 && json.data?.url ? json.data.url : null);
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.on('error', () => resolve(null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = url.startsWith('https') ? https.get(url, handleResponse) : http.get(url, handleResponse);
|
||||||
|
req.on('error', () => resolve(null));
|
||||||
|
req.setTimeout(3000, () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从远程URL下载图片并转为base64
|
||||||
|
async function downloadImageAsBase64(imageUrl: string): Promise<string | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const handleResponse = (res: http.IncomingMessage) => {
|
||||||
|
if (res.statusCode !== 200) return resolve(null);
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
const ext = imageUrl.match(/\.(jpg|jpeg|png|gif|webp)$/i)?.[1] || 'png';
|
||||||
|
const mime = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp' }[ext] || 'png';
|
||||||
|
resolve(`url('data:image/${mime};base64,${buffer.toString('base64')}')`);
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.on('error', () => resolve(null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = imageUrl.startsWith('https') ? https.get(imageUrl, handleResponse) : http.get(imageUrl, handleResponse);
|
||||||
|
req.on('error', () => resolve(null));
|
||||||
|
req.setTimeout(5000, () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取默认开屏图片
|
||||||
|
function getDefaultSplashImage(): string {
|
||||||
|
const path = getResourcePath('../../public/image/splash_screen.png', 'public/image/splash_screen.png');
|
||||||
|
if (existsSync(path)) {
|
||||||
|
const base64 = readFileSync(path).toString('base64');
|
||||||
|
return `url('data:image/png;base64,${base64}')`;
|
||||||
|
}
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
function getDataDirectoryPath(): string {
|
function getDataDirectoryPath(): string {
|
||||||
const dataDir = join(app.getPath('userData'), 'data');
|
const dataDir = join(app.getPath('userData'), 'data');
|
||||||
if (!existsSync(dataDir)) mkdirSync(dataDir, {recursive: true});
|
if (!existsSync(dataDir)) mkdirSync(dataDir, {recursive: true});
|
||||||
@@ -101,6 +247,8 @@ interface AppConfig {
|
|||||||
launchMinimized?: boolean;
|
launchMinimized?: boolean;
|
||||||
lastUsername?: string;
|
lastUsername?: string;
|
||||||
splashImageUrl?: string;
|
splashImageUrl?: string;
|
||||||
|
brandLogoUrl?: string;
|
||||||
|
serverUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigPath(): string {
|
function getConfigPath(): string {
|
||||||
@@ -143,6 +291,7 @@ function migrateDataFromPublic(): void {
|
|||||||
|
|
||||||
|
|
||||||
function startSpringBoot() {
|
function startSpringBoot() {
|
||||||
|
console.log('[Spring Boot] 开始启动...');
|
||||||
migrateDataFromPublic();
|
migrateDataFromPublic();
|
||||||
const jarPath = getJarFilePath();
|
const jarPath = getJarFilePath();
|
||||||
const javaPath = getJavaExecutablePath();
|
const javaPath = getJavaExecutablePath();
|
||||||
@@ -167,21 +316,22 @@ function startSpringBoot() {
|
|||||||
springProcess = spawn(javaPath, springArgs, {
|
springProcess = spawn(javaPath, springArgs, {
|
||||||
cwd: dataDir,
|
cwd: dataDir,
|
||||||
detached: false,
|
detached: false,
|
||||||
stdio: 'ignore'
|
stdio: 'ignore',
|
||||||
|
windowsHide: true
|
||||||
});
|
});
|
||||||
|
|
||||||
let startupCompleted = false;
|
let startupCompleted = false;
|
||||||
|
|
||||||
springProcess.on('close', () => mainWindow ? mainWindow.close() : app.quit());
|
springProcess.on('close', () => mainWindow ? mainWindow.close() : app.quit());
|
||||||
springProcess.on('error', (error) => {
|
springProcess.on('error', (error) => {
|
||||||
dialog.showErrorBox('启动失败', error.message.includes('ENOENT')
|
dialog.showErrorBox('启动失败', error.message.includes('ENOENT')
|
||||||
? '找不到 Java 运行环境'
|
? '找不到 Java 运行环境'
|
||||||
: '启动 Java 应用失败');
|
: '启动 Java 应用失败');
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let checkCount = 0;
|
||||||
const checkHealth = () => {
|
const checkHealth = () => {
|
||||||
if (startupCompleted) return;
|
if (startupCompleted) return;
|
||||||
|
|
||||||
http.get('http://127.0.0.1:8081/api/system/version', (res) => {
|
http.get('http://127.0.0.1:8081/api/system/version', (res) => {
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
setTimeout(checkHealth, 100);
|
setTimeout(checkHealth, 100);
|
||||||
@@ -213,7 +363,6 @@ function startSpringBoot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startSpringBoot();
|
|
||||||
function stopSpringBoot() {
|
function stopSpringBoot() {
|
||||||
if (!springProcess) return;
|
if (!springProcess) return;
|
||||||
try {
|
try {
|
||||||
@@ -236,8 +385,10 @@ function stopSpringBoot() {
|
|||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
|
minWidth: 1200,
|
||||||
|
minHeight: 800,
|
||||||
show: false, //
|
show: false, //
|
||||||
frame: false,
|
frame: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
@@ -306,7 +457,7 @@ if (!gotTheLock) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(async () => {
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
protocol.interceptFileProtocol('file', (request, callback) => {
|
protocol.interceptFileProtocol('file', (request, callback) => {
|
||||||
// 使用 fileURLToPath 正确解码 URL,处理空格和特殊字符
|
// 使用 fileURLToPath 正确解码 URL,处理空格和特殊字符
|
||||||
@@ -317,9 +468,9 @@ app.whenReady().then(() => {
|
|||||||
// 如果解码失败,回退到原来的方法
|
// 如果解码失败,回退到原来的方法
|
||||||
filePath = decodeURIComponent(request.url.substring(8));
|
filePath = decodeURIComponent(request.url.substring(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是 icon 或 image 资源请求
|
// 检查是否是 icon 或 image 资源请求
|
||||||
if (filePath.includes('/icon/') || filePath.includes('\\icon\\') ||
|
if (filePath.includes('/icon/') || filePath.includes('\\icon\\') ||
|
||||||
filePath.includes('/image/') || filePath.includes('\\image\\')) {
|
filePath.includes('/image/') || filePath.includes('\\image\\')) {
|
||||||
const match = filePath.match(/[/\\](icon|image)[/\\]([^?#]+)/);
|
const match = filePath.match(/[/\\](icon|image)[/\\]([^?#]+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -331,15 +482,15 @@ app.whenReady().then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback({ path: filePath });
|
callback({ path: filePath });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用开机自启动配置
|
// 应用开机自启动配置
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const shouldMinimize = config.launchMinimized || false;
|
const shouldMinimize = config.launchMinimized || false;
|
||||||
|
|
||||||
if (config.autoLaunch !== undefined) {
|
if (config.autoLaunch !== undefined) {
|
||||||
app.setLoginItemSettings({
|
app.setLoginItemSettings({
|
||||||
openAtLogin: config.autoLaunch,
|
openAtLogin: config.autoLaunch,
|
||||||
@@ -352,49 +503,93 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
// 只有在不需要最小化启动时才显示 splash 窗口
|
// 只有在不需要最小化启动时才显示 splash 窗口
|
||||||
if (!shouldMinimize) {
|
if (!shouldMinimize) {
|
||||||
|
const config = loadConfig();
|
||||||
|
const username = config.lastUsername || '';
|
||||||
|
const userSplashUrl = config.splashImageUrl || '';
|
||||||
|
|
||||||
|
let splashImage: string | null = null;
|
||||||
|
|
||||||
|
// 开屏图片加载优先级:全局图片(实时) > 用户自定义图片 > 默认本地图片
|
||||||
|
console.log('[开屏图片] 开始获取全局开屏图片...');
|
||||||
|
|
||||||
|
// 1. 获取全局开屏图片
|
||||||
|
const globalUrl = await fetchGlobalSplashImageUrl();
|
||||||
|
if (globalUrl) {
|
||||||
|
console.log('[开屏图片] 获取到全局图片URL:', globalUrl);
|
||||||
|
splashImage = await downloadImageAsBase64(globalUrl);
|
||||||
|
if (splashImage) {
|
||||||
|
console.log('[开屏图片] 使用实时全局图片');
|
||||||
|
downloadImageToLocal(globalUrl, '', 'global_splash').catch(() => {});
|
||||||
|
} else {
|
||||||
|
splashImage = loadCachedImage('', 'global_splash');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
splashImage = loadCachedImage('', 'global_splash');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 如果没有全局图片,尝试用户自定义图片
|
||||||
|
if (!splashImage && userSplashUrl && username) {
|
||||||
|
console.log('[开屏图片] 使用用户自定义图片');
|
||||||
|
splashImage = loadCachedImage(username, 'splash');
|
||||||
|
if (!splashImage) {
|
||||||
|
downloadImageToLocal(userSplashUrl, username, 'splash').catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 使用默认本地图片
|
||||||
|
if (!splashImage) {
|
||||||
|
console.log('[开屏图片] 使用默认本地图片');
|
||||||
|
splashImage = getDefaultSplashImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将图片数据写入临时文件,避免 data URL 过长
|
||||||
|
const tempSplashPath = join(app.getPath('temp'), 'splash-temp.html');
|
||||||
|
const splashHtml = readFileSync(getSplashPath(), 'utf-8').replace('__SPLASH_IMAGE__', splashImage);
|
||||||
|
writeFileSync(tempSplashPath, splashHtml);
|
||||||
|
|
||||||
|
// 记录 splash 显示时间
|
||||||
|
splashStartTime = Date.now();
|
||||||
|
|
||||||
splashWindow = new BrowserWindow({
|
splashWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 675,
|
height: 800,
|
||||||
frame: false,
|
frame: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
alwaysOnTop: false,
|
alwaysOnTop: true, // 设置为置顶,确保在主窗口之上
|
||||||
show: true,
|
show: false, // 创建时不显示,等待内容加载完成
|
||||||
center: true,
|
center: true,
|
||||||
icon: getIconPath(),
|
icon: getIconPath(),
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: true,
|
||||||
contextIsolation: true,
|
contextIsolation: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听启动窗口关闭事件
|
splashWindow.on('closed', () => splashWindow = null);
|
||||||
splashWindow.on('closed', () => {
|
|
||||||
splashWindow = null;
|
// 监听页面加载完成后显示
|
||||||
|
splashWindow.webContents.on('did-finish-load', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (splashWindow && !splashWindow.isDestroyed()) {
|
||||||
|
splashWindow.show();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
const splashPath = getSplashPath();
|
// 加载临时 HTML 文件
|
||||||
if (existsSync(splashPath)) {
|
splashWindow.loadFile(tempSplashPath);
|
||||||
const config = loadConfig();
|
|
||||||
const imageUrl = config.splashImageUrl || '';
|
|
||||||
console.log('[开屏图片] 启动配置:', { username: config.lastUsername, imageUrl, configPath: getConfigPath() });
|
|
||||||
|
|
||||||
splashWindow.loadFile(splashPath);
|
|
||||||
|
|
||||||
if (imageUrl) {
|
|
||||||
splashWindow.webContents.once('did-finish-load', () => {
|
|
||||||
splashWindow?.webContents.executeJavaScript(`
|
|
||||||
document.body.style.setProperty('--splash-image', "url('${imageUrl}')");
|
|
||||||
`).then(() => console.log('[开屏图片] 注入成功:', imageUrl))
|
|
||||||
.catch(err => console.error('[开屏图片] 注入失败:', err));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//666
|
|
||||||
|
// 已手动启动后端
|
||||||
|
setTimeout(() => {
|
||||||
|
startSpringBoot();
|
||||||
|
}, 200);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
openAppIfNotOpened();
|
openAppIfNotOpened();
|
||||||
}, 100);
|
}, 200);
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
@@ -424,6 +619,11 @@ ipcMain.on('message', (event, message) => {
|
|||||||
console.log(message);
|
console.log(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on('quit-app', () => {
|
||||||
|
isQuitting = true;
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-jar-version', () => {
|
ipcMain.handle('get-jar-version', () => {
|
||||||
const jarPath = getJarFilePath();
|
const jarPath = getJarFilePath();
|
||||||
const match = jarPath ? basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/) : null;
|
const match = jarPath ? basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/) : null;
|
||||||
@@ -758,6 +958,18 @@ ipcMain.handle('set-launch-config', (event, launchConfig: { autoLaunch: boolean;
|
|||||||
// 刷新页面
|
// 刷新页面
|
||||||
ipcMain.handle('reload', () => mainWindow?.webContents.reload());
|
ipcMain.handle('reload', () => mainWindow?.webContents.reload());
|
||||||
|
|
||||||
|
// 开发模式:跳过后端启动
|
||||||
|
ipcMain.handle('dev-skip-backend', () => {
|
||||||
|
if (isDev) {
|
||||||
|
console.log('[开发模式] 前端请求跳过后端启动');
|
||||||
|
openAppIfNotOpened();
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return { success: false, error: '仅开发模式可用' };
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 窗口控制 API
|
// 窗口控制 API
|
||||||
ipcMain.handle('window-minimize', () => {
|
ipcMain.handle('window-minimize', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
@@ -785,13 +997,21 @@ ipcMain.handle('window-is-maximized', () => {
|
|||||||
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isMaximized() : false;
|
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isMaximized() : false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保存开屏图片配置(用户名 + URL)
|
// 保存开屏图片配置(用户名 + URL)并下载到本地
|
||||||
ipcMain.handle('save-splash-config', (event, username: string, imageUrl: string) => {
|
ipcMain.handle('save-splash-config', async (event, username: string, imageUrl: string) => {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
config.lastUsername = username;
|
config.lastUsername = username;
|
||||||
config.splashImageUrl = imageUrl;
|
config.splashImageUrl = imageUrl;
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
console.log('[开屏图片] 已保存配置:', { username, imageUrl, path: getConfigPath() });
|
|
||||||
|
// 如果有图片URL,立即下载到本地缓存
|
||||||
|
if (imageUrl && username) {
|
||||||
|
await downloadImageToLocal(imageUrl, username, 'splash');
|
||||||
|
} else if (username) {
|
||||||
|
// 如果图片URL为空,删除本地缓存
|
||||||
|
deleteCachedImage(username, 'splash');
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -801,12 +1021,52 @@ ipcMain.handle('get-splash-config', () => {
|
|||||||
return { username: config.lastUsername || '', imageUrl: config.splashImageUrl || '' };
|
return { username: config.lastUsername || '', imageUrl: config.splashImageUrl || '' };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 保存品牌logo配置
|
||||||
|
ipcMain.handle('save-brand-logo-config', async (event, username: string, logoUrl: string) => {
|
||||||
|
const config = loadConfig();
|
||||||
|
config.brandLogoUrl = logoUrl;
|
||||||
|
saveConfig(config);
|
||||||
|
|
||||||
|
// 如果有logo URL,立即下载到本地缓存
|
||||||
|
if (logoUrl && username) {
|
||||||
|
await downloadImageToLocal(logoUrl, username, 'logo');
|
||||||
|
} else if (username) {
|
||||||
|
// 如果logo URL为空,删除本地缓存
|
||||||
|
deleteCachedImage(username, 'logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清除用户配置(退出登录时调用)
|
||||||
|
ipcMain.handle('clear-user-config', async () => {
|
||||||
|
const config = loadConfig();
|
||||||
|
const username = config.lastUsername;
|
||||||
|
|
||||||
|
// 清除配置
|
||||||
|
config.lastUsername = '';
|
||||||
|
config.splashImageUrl = '';
|
||||||
|
config.brandLogoUrl = '';
|
||||||
|
saveConfig(config);
|
||||||
|
|
||||||
|
// 删除本地缓存的图片
|
||||||
|
if (username) {
|
||||||
|
deleteCachedImage(username, 'splash');
|
||||||
|
deleteCachedImage(username, 'logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载完整配置
|
||||||
|
ipcMain.handle('load-config', () => {
|
||||||
|
return loadConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
async function getFileSize(url: string): Promise<number> {
|
async function getFileSize(url: string): Promise<number> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const protocol = url.startsWith('https') ? https : http;
|
const handleResponse = (response: http.IncomingMessage) => {
|
||||||
|
|
||||||
const request = protocol.get(url, {method: 'HEAD'}, (response) => {
|
|
||||||
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
|
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
|
||||||
const redirectUrl = response.headers.location;
|
const redirectUrl = response.headers.location;
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
@@ -814,11 +1074,16 @@ async function getFileSize(url: string): Promise<number> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = parseInt(response.headers['content-length'] || '0', 10);
|
const size = parseInt(response.headers['content-length'] || '0', 10);
|
||||||
resolve(size);
|
resolve(size);
|
||||||
}).on('error', () => resolve(0));
|
};
|
||||||
|
|
||||||
|
const request = url.startsWith('https')
|
||||||
|
? https.get(url, {method: 'HEAD'}, handleResponse)
|
||||||
|
: http.get(url, {method: 'HEAD'}, handleResponse);
|
||||||
|
|
||||||
|
request.on('error', () => resolve(0));
|
||||||
request.setTimeout(10000, () => {
|
request.setTimeout(10000, () => {
|
||||||
request.destroy();
|
request.destroy();
|
||||||
resolve(0);
|
resolve(0);
|
||||||
@@ -828,9 +1093,9 @@ async function getFileSize(url: string): Promise<number> {
|
|||||||
|
|
||||||
async function downloadFile(url: string, filePath: string, onProgress: (progress: {downloaded: number, total: number}) => void): Promise<void> {
|
async function downloadFile(url: string, filePath: string, onProgress: (progress: {downloaded: number, total: number}) => void): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const protocol = url.startsWith('https') ? https : http;
|
let request: http.ClientRequest;
|
||||||
|
|
||||||
const request = protocol.get(url, (response) => {
|
const handleResponse = (response: http.IncomingMessage) => {
|
||||||
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
|
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
|
||||||
const redirectUrl = response.headers.location;
|
const redirectUrl = response.headers.location;
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
@@ -838,7 +1103,7 @@ async function downloadFile(url: string, filePath: string, onProgress: (progress
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode !== 200) {
|
if (response.statusCode !== 200) {
|
||||||
reject(new Error(`HTTP ${response.statusCode}`));
|
reject(new Error(`HTTP ${response.statusCode}`));
|
||||||
return;
|
return;
|
||||||
@@ -873,7 +1138,12 @@ async function downloadFile(url: string, filePath: string, onProgress: (progress
|
|||||||
fs.unlink(filePath).catch(() => {});
|
fs.unlink(filePath).catch(() => {});
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
}).on('error', reject);
|
request = url.startsWith('https')
|
||||||
|
? https.get(url, handleResponse)
|
||||||
|
: http.get(url, handleResponse);
|
||||||
|
|
||||||
|
request.on('error', reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const electronAPI = {
|
|||||||
// 添加日志相关 API
|
// 添加日志相关 API
|
||||||
getLogDates: () => ipcRenderer.invoke('get-log-dates'),
|
getLogDates: () => ipcRenderer.invoke('get-log-dates'),
|
||||||
readLogFile: (logDate: string) => ipcRenderer.invoke('read-log-file', logDate),
|
readLogFile: (logDate: string) => ipcRenderer.invoke('read-log-file', logDate),
|
||||||
|
|
||||||
// 关闭行为配置 API
|
// 关闭行为配置 API
|
||||||
getCloseAction: () => ipcRenderer.invoke('get-close-action'),
|
getCloseAction: () => ipcRenderer.invoke('get-close-action'),
|
||||||
setCloseAction: (action: 'quit' | 'minimize' | 'tray') => ipcRenderer.invoke('set-close-action', action),
|
setCloseAction: (action: 'quit' | 'minimize' | 'tray') => ipcRenderer.invoke('set-close-action', action),
|
||||||
@@ -47,6 +46,11 @@ const electronAPI = {
|
|||||||
saveSplashConfig: (username: string, imageUrl: string) => ipcRenderer.invoke('save-splash-config', username, imageUrl),
|
saveSplashConfig: (username: string, imageUrl: string) => ipcRenderer.invoke('save-splash-config', username, imageUrl),
|
||||||
getSplashConfig: () => ipcRenderer.invoke('get-splash-config'),
|
getSplashConfig: () => ipcRenderer.invoke('get-splash-config'),
|
||||||
|
|
||||||
|
// 品牌logo相关 API
|
||||||
|
saveBrandLogoConfig: (username: string, logoUrl: string) => ipcRenderer.invoke('save-brand-logo-config', username, logoUrl),
|
||||||
|
loadConfig: () => ipcRenderer.invoke('load-config'),
|
||||||
|
clearUserConfig: () => ipcRenderer.invoke('clear-user-config'),
|
||||||
|
|
||||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||||
ipcRenderer.removeAllListeners('download-progress')
|
ipcRenderer.removeAllListeners('download-progress')
|
||||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
||||||
|
|||||||
@@ -32,10 +32,8 @@ export function createTray(mainWindow: BrowserWindow | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 右键菜单
|
// 右键菜单
|
||||||
updateTrayMenu(mainWindow)
|
updateTrayMenu(mainWindow)
|
||||||
|
|
||||||
return tray
|
return tray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import {authApi} from './api/auth'
|
import {authApi} from './api/auth'
|
||||||
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
||||||
|
import {splashApi} from './api/splash'
|
||||||
import {getOrCreateDeviceId} from './utils/deviceId'
|
import {getOrCreateDeviceId} from './utils/deviceId'
|
||||||
import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token'
|
import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken, getRegisterTimeFromToken} from './utils/token'
|
||||||
import {CONFIG} from './api/http'
|
import {CONFIG} from './api/http'
|
||||||
import {getSettings} from './utils/settings'
|
import {getSettings} from './utils/settings'
|
||||||
import LoginDialog from './components/auth/LoginDialog.vue'
|
import LoginDialog from './components/auth/LoginDialog.vue'
|
||||||
@@ -50,6 +51,7 @@ const showAuthDialog = ref(false)
|
|||||||
const showRegDialog = ref(false)
|
const showRegDialog = ref(false)
|
||||||
const zhCnLocale = zhCn
|
const zhCnLocale = zhCn
|
||||||
const currentUsername = ref('')
|
const currentUsername = ref('')
|
||||||
|
const registerTime = ref('')
|
||||||
const showDeviceDialog = ref(false)
|
const showDeviceDialog = ref(false)
|
||||||
const deviceLoading = ref(false)
|
const deviceLoading = ref(false)
|
||||||
const devices = ref<DeviceItem[]>([])
|
const devices = ref<DeviceItem[]>([])
|
||||||
@@ -61,12 +63,12 @@ const vipExpireTime = ref<Date | null>(null)
|
|||||||
const deviceTrialExpired = ref(false)
|
const deviceTrialExpired = ref(false)
|
||||||
const accountType = ref<string>('trial')
|
const accountType = ref<string>('trial')
|
||||||
const vipStatus = computed(() => {
|
const vipStatus = computed(() => {
|
||||||
if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType: 'account' }
|
if (!vipExpireTime.value) return {isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType: 'account'}
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const expire = new Date(vipExpireTime.value)
|
const expire = new Date(vipExpireTime.value)
|
||||||
const msLeft = expire.getTime() - now.getTime()
|
const msLeft = expire.getTime() - now.getTime()
|
||||||
|
|
||||||
// 精确判断:当前时间 >= 过期时间,则已过期(与后端逻辑一致)
|
// 精确判断:当前时间 >= 过期时间,则已过期(与后端逻辑一致)
|
||||||
if (msLeft <= 0) {
|
if (msLeft <= 0) {
|
||||||
const accountExpired = true
|
const accountExpired = true
|
||||||
@@ -75,22 +77,22 @@ const vipStatus = computed(() => {
|
|||||||
if (deviceExpired && accountExpired) expiredType = 'both'
|
if (deviceExpired && accountExpired) expiredType = 'both'
|
||||||
else if (accountExpired) expiredType = 'account'
|
else if (accountExpired) expiredType = 'account'
|
||||||
else if (deviceExpired) expiredType = 'device'
|
else if (deviceExpired) expiredType = 'device'
|
||||||
|
|
||||||
return { isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType }
|
return {isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoursLeft = Math.floor(msLeft / (1000 * 60 * 60))
|
const hoursLeft = Math.floor(msLeft / (1000 * 60 * 60))
|
||||||
const daysLeft = Math.floor(msLeft / (1000 * 60 * 60 * 24))
|
const daysLeft = Math.floor(msLeft / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
let expiredType: 'device' | 'account' | 'both' | 'subscribe' = 'subscribe'
|
let expiredType: 'device' | 'account' | 'both' | 'subscribe' = 'subscribe'
|
||||||
if (accountType.value === 'trial' && deviceTrialExpired.value) {
|
if (accountType.value === 'trial' && deviceTrialExpired.value) {
|
||||||
expiredType = 'device' // 试用账号且设备过期
|
expiredType = 'device' // 试用账号且设备过期
|
||||||
}
|
}
|
||||||
|
|
||||||
if (daysLeft === 0) return { isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType }
|
if (daysLeft === 0) return {isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType}
|
||||||
if (daysLeft <= 7) return { isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType }
|
if (daysLeft <= 7) return {isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType}
|
||||||
if (daysLeft <= 30) return { isVip: true, daysLeft, hoursLeft, status: 'normal', expiredType }
|
if (daysLeft <= 30) return {isVip: true, daysLeft, hoursLeft, status: 'normal', expiredType}
|
||||||
return { isVip: true, daysLeft, hoursLeft, status: 'active', expiredType }
|
return {isVip: true, daysLeft, hoursLeft, status: 'active', expiredType}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 功能可用性(账号VIP + 设备试用期)
|
// 功能可用性(账号VIP + 设备试用期)
|
||||||
@@ -118,6 +120,9 @@ const showAccountManager = ref(false)
|
|||||||
// 当前版本
|
// 当前版本
|
||||||
const currentVersion = ref('')
|
const currentVersion = ref('')
|
||||||
|
|
||||||
|
// 品牌logo
|
||||||
|
const brandLogoUrl = ref('')
|
||||||
|
|
||||||
// 菜单配置 - 复刻ERP客户端格式
|
// 菜单配置 - 复刻ERP客户端格式
|
||||||
const menuConfig = [
|
const menuConfig = [
|
||||||
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R', iconImage: rakutenIcon},
|
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R', iconImage: rakutenIcon},
|
||||||
@@ -198,19 +203,26 @@ function handleMenuSelect(key: string) {
|
|||||||
addToHistory(key)
|
addToHistory(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }) {
|
async function handleLoginSuccess(data: {
|
||||||
|
token: string;
|
||||||
|
permissions?: string;
|
||||||
|
expireTime?: string;
|
||||||
|
accountType?: string;
|
||||||
|
deviceTrialExpired?: boolean
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
isAuthenticated.value = true
|
isAuthenticated.value = true
|
||||||
showAuthDialog.value = false
|
showAuthDialog.value = false
|
||||||
showRegDialog.value = false
|
showRegDialog.value = false
|
||||||
|
|
||||||
currentUsername.value = getUsernameFromToken(data.token)
|
currentUsername.value = getUsernameFromToken(data.token)
|
||||||
userPermissions.value = data.permissions || ''
|
userPermissions.value = data.permissions || ''
|
||||||
vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null
|
vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null
|
||||||
accountType.value = data.accountType || 'trial'
|
accountType.value = data.accountType || 'trial'
|
||||||
deviceTrialExpired.value = data.deviceTrialExpired || false
|
deviceTrialExpired.value = data.deviceTrialExpired || false
|
||||||
|
registerTime.value = getRegisterTimeFromToken(data.token)
|
||||||
|
|
||||||
const deviceId = await getOrCreateDeviceId()
|
const deviceId = await getOrCreateDeviceId()
|
||||||
await deviceApi.register({
|
await deviceApi.register({
|
||||||
username: currentUsername.value,
|
username: currentUsername.value,
|
||||||
@@ -218,7 +230,7 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
|
|||||||
os: navigator.platform
|
os: navigator.platform
|
||||||
})
|
})
|
||||||
SSEManager.connect()
|
SSEManager.connect()
|
||||||
|
|
||||||
// 同步当前账号的设置到 Electron 主进程
|
// 同步当前账号的设置到 Electron 主进程
|
||||||
syncSettingsToElectron()
|
syncSettingsToElectron()
|
||||||
|
|
||||||
@@ -231,31 +243,40 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
|
|||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
removeToken()
|
removeToken()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLocalAuth() {
|
async function clearLocalAuth() {
|
||||||
removeToken()
|
removeToken()
|
||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
currentUsername.value = ''
|
currentUsername.value = ''
|
||||||
|
registerTime.value = ''
|
||||||
userPermissions.value = ''
|
userPermissions.value = ''
|
||||||
vipExpireTime.value = null
|
vipExpireTime.value = null
|
||||||
deviceTrialExpired.value = false
|
deviceTrialExpired.value = false
|
||||||
accountType.value = 'trial'
|
accountType.value = 'trial'
|
||||||
|
brandLogoUrl.value = '' // 清除品牌logo
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
showDeviceDialog.value = false
|
showDeviceDialog.value = false
|
||||||
SSEManager.disconnect()
|
SSEManager.disconnect()
|
||||||
|
|
||||||
|
// 清除主进程中的用户配置和缓存
|
||||||
|
try {
|
||||||
|
await (window as any).electronAPI.clearUserConfig()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('清除用户配置失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
try {
|
||||||
const deviceId = getClientIdFromToken()
|
const deviceId = getClientIdFromToken()
|
||||||
if (deviceId) await deviceApi.offline({ deviceId, username: currentUsername.value })
|
if (deviceId) await deviceApi.offline({deviceId, username: currentUsername.value})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('离线通知失败:', error)
|
console.warn('离线通知失败:', error)
|
||||||
}
|
}
|
||||||
clearLocalAuth()
|
await clearLocalAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUserClick() {
|
async function handleUserClick() {
|
||||||
@@ -270,7 +291,8 @@ async function handleUserClick() {
|
|||||||
cancelButtonText: '取消'
|
cancelButtonText: '取消'
|
||||||
})
|
})
|
||||||
await logout()
|
await logout()
|
||||||
} catch {}
|
} catch {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRegisterDialog() {
|
function showRegisterDialog() {
|
||||||
@@ -280,7 +302,7 @@ function showRegisterDialog() {
|
|||||||
|
|
||||||
function backToLogin() {
|
function backToLogin() {
|
||||||
showRegDialog.value = false
|
showRegDialog.value = false
|
||||||
|
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,13 +322,14 @@ async function checkAuth() {
|
|||||||
userPermissions.value = res.data.permissions || ''
|
userPermissions.value = res.data.permissions || ''
|
||||||
deviceTrialExpired.value = res.data.deviceTrialExpired || false
|
deviceTrialExpired.value = res.data.deviceTrialExpired || false
|
||||||
accountType.value = res.data.accountType || 'trial'
|
accountType.value = res.data.accountType || 'trial'
|
||||||
|
registerTime.value = getRegisterTimeFromToken(token)
|
||||||
|
|
||||||
if (res.data.expireTime) {
|
if (res.data.expireTime) {
|
||||||
vipExpireTime.value = new Date(res.data.expireTime)
|
vipExpireTime.value = new Date(res.data.expireTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
SSEManager.connect()
|
SSEManager.connect()
|
||||||
|
|
||||||
// 同步当前账号的设置到 Electron 主进程
|
// 同步当前账号的设置到 Electron 主进程
|
||||||
syncSettingsToElectron()
|
syncSettingsToElectron()
|
||||||
} catch {
|
} catch {
|
||||||
@@ -322,7 +345,7 @@ async function refreshVipStatus() {
|
|||||||
try {
|
try {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) return false
|
if (!token) return false
|
||||||
|
|
||||||
const res = await authApi.verifyToken(token)
|
const res = await authApi.verifyToken(token)
|
||||||
deviceTrialExpired.value = res.data.deviceTrialExpired || false
|
deviceTrialExpired.value = res.data.deviceTrialExpired || false
|
||||||
accountType.value = res.data.accountType || 'trial'
|
accountType.value = res.data.accountType || 'trial'
|
||||||
@@ -346,10 +369,10 @@ async function syncSettingsToElectron() {
|
|||||||
try {
|
try {
|
||||||
const username = getUsernameFromToken()
|
const username = getUsernameFromToken()
|
||||||
const settings = getSettings(username)
|
const settings = getSettings(username)
|
||||||
|
|
||||||
// 同步关闭行为
|
// 同步关闭行为
|
||||||
await (window as any).electronAPI.setCloseAction(settings.closeAction || 'quit')
|
await (window as any).electronAPI.setCloseAction(settings.closeAction || 'quit')
|
||||||
|
|
||||||
// 同步启动配置
|
// 同步启动配置
|
||||||
await (window as any).electronAPI.setLaunchConfig({
|
await (window as any).electronAPI.setLaunchConfig({
|
||||||
autoLaunch: settings.autoLaunch || false,
|
autoLaunch: settings.autoLaunch || false,
|
||||||
@@ -360,6 +383,30 @@ async function syncSettingsToElectron() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载品牌logo
|
||||||
|
async function loadBrandLogo() {
|
||||||
|
try {
|
||||||
|
// 1. 优先从本地缓存读取(秒开)
|
||||||
|
const config = await (window as any).electronAPI.loadConfig()
|
||||||
|
if (config.brandLogoUrl) {
|
||||||
|
brandLogoUrl.value = config.brandLogoUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 后台异步更新(不阻塞UI)
|
||||||
|
const username = getUsernameFromToken()
|
||||||
|
if (username) {
|
||||||
|
const res = await splashApi.getBrandLogo(username)
|
||||||
|
const newUrl = res.data.url
|
||||||
|
if (newUrl !== brandLogoUrl.value) {
|
||||||
|
brandLogoUrl.value = newUrl
|
||||||
|
await (window as any).electronAPI.saveBrandLogoConfig(username, newUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
brandLogoUrl.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 提供给子组件使用
|
// 提供给子组件使用
|
||||||
provide('refreshVipStatus', refreshVipStatus)
|
provide('refreshVipStatus', refreshVipStatus)
|
||||||
provide('vipStatus', vipStatus)
|
provide('vipStatus', vipStatus)
|
||||||
@@ -378,7 +425,7 @@ const SSEManager = {
|
|||||||
|
|
||||||
const src = new EventSource(`${CONFIG.SSE_URL}?clientId=${clientId}&token=${token}`)
|
const src = new EventSource(`${CONFIG.SSE_URL}?clientId=${clientId}&token=${token}`)
|
||||||
this.connection = src
|
this.connection = src
|
||||||
src.onopen = () => console.log('SSE连接已建立')
|
src.onopen = () => {}
|
||||||
src.onmessage = (e) => this.handleMessage(e)
|
src.onmessage = (e) => this.handleMessage(e)
|
||||||
src.onerror = () => this.handleError()
|
src.onerror = () => this.handleError()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -387,22 +434,20 @@ const SSEManager = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMessage(e: MessageEvent) {
|
async handleMessage(e: MessageEvent) {
|
||||||
try {
|
try {
|
||||||
if (e.type === 'ping') return
|
if (e.type === 'ping') return
|
||||||
|
|
||||||
console.log('SSE消息:', e.data)
|
|
||||||
const payload = JSON.parse(e.data)
|
const payload = JSON.parse(e.data)
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case 'ready':
|
case 'ready':
|
||||||
console.log('SSE连接已就绪')
|
|
||||||
break
|
break
|
||||||
case 'DEVICE_REMOVED':
|
case 'DEVICE_REMOVED':
|
||||||
clearLocalAuth()
|
await clearLocalAuth()
|
||||||
ElMessage.warning('会话已失效,请重新登录')
|
ElMessage.warning('会话已失效,请重新登录')
|
||||||
break
|
break
|
||||||
case 'FORCE_LOGOUT':
|
case 'FORCE_LOGOUT':
|
||||||
logout()
|
await logout()
|
||||||
ElMessage.warning('会话已失效,请重新登录')
|
ElMessage.warning('会话已失效,请重新登录')
|
||||||
break
|
break
|
||||||
case 'PERMISSIONS_UPDATED':
|
case 'PERMISSIONS_UPDATED':
|
||||||
@@ -505,7 +550,7 @@ async function confirmRemoveDevice(row: DeviceItem) {
|
|||||||
deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1)
|
deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1)
|
||||||
|
|
||||||
if (row.deviceId === getClientIdFromToken()) {
|
if (row.deviceId === getClientIdFromToken()) {
|
||||||
clearLocalAuth()
|
await clearLocalAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success('已移除设备')
|
ElMessage.success('已移除设备')
|
||||||
@@ -518,24 +563,32 @@ async function confirmRemoveDevice(row: DeviceItem) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
showContent()
|
showContent()
|
||||||
await checkAuth()
|
await checkAuth()
|
||||||
|
|
||||||
// 检查是否有待安装的更新
|
// 检查是否有待安装的更新
|
||||||
await checkPendingUpdate()
|
await checkPendingUpdate()
|
||||||
|
|
||||||
// 加载当前版本
|
// 加载当前版本
|
||||||
try {
|
try {
|
||||||
currentVersion.value = await (window as any).electronAPI.getJarVersion()
|
currentVersion.value = await (window as any).electronAPI.getJarVersion()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('获取当前版本失败:', error)
|
console.warn('获取当前版本失败:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载品牌logo
|
||||||
|
loadBrandLogo()
|
||||||
|
|
||||||
|
// 监听品牌logo变化
|
||||||
|
window.addEventListener('brandLogoChanged', (e: any) => {
|
||||||
|
brandLogoUrl.value = e.detail
|
||||||
|
})
|
||||||
|
|
||||||
// 全局阻止文件拖拽到窗口(避免意外打开文件)
|
// 全局阻止文件拖拽到窗口(避免意外打开文件)
|
||||||
// 只在指定的 dropzone 区域处理拖拽上传
|
// 只在指定的 dropzone 区域处理拖拽上传
|
||||||
document.addEventListener('dragover', (e) => {
|
document.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
document.addEventListener('drop', (e) => {
|
document.addEventListener('drop', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -581,9 +634,32 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="erp-container">
|
<div class="erp-container">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="user-avatar">
|
<div class="main-logo">
|
||||||
<img src="/icon/icon.png" alt="logo"/>
|
<img src="/icon/icon.png" alt="logo"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户头像区域 -->
|
||||||
|
<div class="user-avatar-section" @click="handleUserClick">
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<img
|
||||||
|
src="/image/user.png"
|
||||||
|
alt="用户头像"
|
||||||
|
class="user-avatar-img"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-name-wrapper">
|
||||||
|
<span class="user-name">{{ isAuthenticated ? currentUsername : '登录/注册' }}</span>
|
||||||
|
<span v-if="isAuthenticated && vipStatus.isVip" class="vip-badge">VIP {{ vipStatus.daysLeft }}天</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-action">{{ isAuthenticated ? ` ${registerTime ? registerTime.replace('T', ' ').substring(0, 16) : '未知'}` : '登录账号体验完整功能' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brand-logo-section">
|
||||||
|
<img src="https://qiniu.bydj.tashowz.com/brand-logo/2026/01/4bfd767a56a54351a6db284563b4f83d.png" alt="品牌 Banner" class="brand-logo"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="menu-group-title">电商平台</div>
|
<div class="menu-group-title">电商平台</div>
|
||||||
<ul class="menu">
|
<ul class="menu">
|
||||||
<li
|
<li
|
||||||
@@ -595,7 +671,7 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<span class="menu-text">
|
<span class="menu-text">
|
||||||
<span class="menu-icon" :data-k="item.key">
|
<span class="menu-icon" :data-k="item.key">
|
||||||
<img v-if="item.iconImage" :src="item.iconImage" :alt="item.name" class="menu-icon-img" />
|
<img v-if="item.iconImage" :src="item.iconImage" :alt="item.name" class="menu-icon-img"/>
|
||||||
<template v-else>{{ item.icon }}</template>
|
<template v-else>{{ item.icon }}</template>
|
||||||
</span>
|
</span>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
@@ -604,7 +680,8 @@ onUnmounted(() => {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- VIP状态卡片 -->
|
<!-- VIP状态卡片 -->
|
||||||
<div v-if="isAuthenticated" class="vip-status-card" :class="'vip-' + vipStatus.status" @click="openSubscriptionDialog">
|
<div v-if="isAuthenticated" class="vip-status-card" :class="'vip-' + vipStatus.status"
|
||||||
|
@click="openSubscriptionDialog">
|
||||||
<div class="vip-info">
|
<div class="vip-info">
|
||||||
<div class="vip-status-text">
|
<div class="vip-status-text">
|
||||||
<template v-if="vipStatus.isVip">
|
<template v-if="vipStatus.isVip">
|
||||||
@@ -616,7 +693,13 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="vip-expire-date" v-if="vipExpireTime">
|
<div class="vip-expire-date" v-if="vipExpireTime">
|
||||||
有效期至:{{ new Date(vipExpireTime).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) }}
|
有效期至:{{
|
||||||
|
new Date(vipExpireTime).toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -638,8 +721,7 @@ onUnmounted(() => {
|
|||||||
@open-device="openDeviceManager"
|
@open-device="openDeviceManager"
|
||||||
@open-settings="openSettings"
|
@open-settings="openSettings"
|
||||||
@open-account-manager="openAccountManager"
|
@open-account-manager="openAccountManager"
|
||||||
@check-update="handleCheckUpdate"
|
@check-update="handleCheckUpdate"/>
|
||||||
@show-login="showAuthDialog = true"/>
|
|
||||||
<div class="content-body">
|
<div class="content-body">
|
||||||
<div
|
<div
|
||||||
class="dashboard-home"
|
class="dashboard-home"
|
||||||
@@ -672,16 +754,20 @@ onUnmounted(() => {
|
|||||||
@back-to-login="backToLogin"/>
|
@back-to-login="backToLogin"/>
|
||||||
|
|
||||||
<!-- 更新对话框 -->
|
<!-- 更新对话框 -->
|
||||||
<UpdateDialog ref="updateDialogRef" v-model="showUpdateDialog" />
|
<UpdateDialog ref="updateDialogRef" v-model="showUpdateDialog"/>
|
||||||
|
|
||||||
<!-- 设置对话框 -->
|
<!-- 设置对话框 -->
|
||||||
<SettingsDialog v-model="showSettingsDialog" @auto-update-changed="handleAutoUpdateChanged" @open-update-dialog="handleOpenUpdateDialog" />
|
<SettingsDialog
|
||||||
|
v-model="showSettingsDialog"
|
||||||
|
:is-vip="canUseFunctions"
|
||||||
|
@auto-update-changed="handleAutoUpdateChanged"
|
||||||
|
@open-update-dialog="handleOpenUpdateDialog" />
|
||||||
|
|
||||||
<!-- 试用期过期弹框 -->
|
<!-- 试用期过期弹框 -->
|
||||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType"/>
|
||||||
|
|
||||||
<!-- 账号管理弹框 -->
|
<!-- 账号管理弹框 -->
|
||||||
<AccountManager v-model="showAccountManager" platform="zebra" />
|
<AccountManager v-model="showAccountManager" platform="zebra"/>
|
||||||
|
|
||||||
<!-- 设备管理弹框 -->
|
<!-- 设备管理弹框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -693,7 +779,9 @@ onUnmounted(() => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="device-dialog-header">
|
<div class="device-dialog-header">
|
||||||
<img src="/icon/img.png" alt="devices" class="device-illustration"/>
|
<img src="/icon/img.png" alt="devices" class="device-illustration"/>
|
||||||
<div class="device-title">设备管理 <span class="device-count">({{ deviceQuota.used || 0 }}/{{ deviceQuota.limit || 0 }})</span></div>
|
<div class="device-title">设备管理 <span class="device-count">({{
|
||||||
|
deviceQuota.used || 0
|
||||||
|
}}/{{ deviceQuota.limit || 0 }})</span></div>
|
||||||
<div class="device-subtitle">当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备</div>
|
<div class="device-subtitle">当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -735,350 +823,80 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.root {position: fixed; inset: 0; width: 100vw; height: 100vh; background-color: #f5f5f5; opacity: 0; transition: opacity 0.1s ease;}
|
||||||
.root {
|
.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;}
|
||||||
position: fixed;
|
.loading-spinner {width: 50px; height: 50px; border: 5px solid #e6e6e6; border-top: 5px solid #409EFF; border-radius: 50%; animation: spin 1s linear infinite;}
|
||||||
inset: 0;
|
@keyframes spin {0% {
|
||||||
width: 100vw;
|
transform: rotate(0deg);}
|
||||||
height: 100vh;
|
100% {transform: rotate(360deg);}
|
||||||
background-color: #f5f5f5;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-container {
|
.erp-container {display: flex; height: 100vh;}
|
||||||
display: flex;
|
.sidebar {width: 220px; min-width: 220px; flex-shrink: 0; background: #F0F0F0; border-right: 1px solid #e8eaec; padding: 16px 12px; box-sizing: border-box; display: flex; flex-direction: column;}
|
||||||
justify-content: center;
|
.platform-icons {display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px;}
|
||||||
align-items: center;
|
.picon {width: 28px; height: 28px; object-fit: contain;}
|
||||||
height: 100vh;
|
/* 主Logo */
|
||||||
width: 100%;
|
.main-logo {align-items: center; justify-content: center; padding: 8px 0; margin: 0 0 16px 0;}
|
||||||
position: fixed;
|
.main-logo img {width: 120px; object-fit: contain;}
|
||||||
top: 0;
|
/* 用户头像区域 */
|
||||||
left: 0;
|
.user-avatar-section {display: flex; align-items: center; gap: 7px; padding: 8px 10px; margin: 0 0 16px 0; border-radius: 8px; cursor: pointer; transition: all 0.2s ease;}
|
||||||
background-color: #f5f5f5;
|
.avatar-wrapper {flex-shrink: 0; width: 40px; height: 40px; border-radius: 50%; overflow: hidden; background: #fff; display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);}
|
||||||
z-index: 9999;
|
.user-avatar-img {width: 100%; height: 100%; object-fit: cover;}
|
||||||
transition: opacity 0.1s ease;
|
.user-info {flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; text-align: left;}
|
||||||
}
|
.user-name-wrapper {display: flex; align-items: center; gap: 4px; min-width: 0;}
|
||||||
|
.user-name {font-size: 14px; font-weight: 600; color: #303133; white-space: nowrap; line-height: 1.2; min-width: 0; overflow: hidden; text-overflow: ellipsis;}
|
||||||
.loading-spinner {
|
.vip-badge {display: inline-flex; align-items: center; justify-content: center; padding: 0px 3px; height: 14px; background: #BAE0FF; border: 1px solid rgba(22, 119, 255, 0.05); border-radius: 8px; font-size: 10px; font-weight: 600; color: rgba(0, 29, 102, 1); white-space: nowrap; flex-shrink: 0; line-height: 100%; text-align: center; letter-spacing: 0%;}
|
||||||
width: 50px;
|
.user-action {font-size: 11px; color: #909399; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
|
||||||
height: 50px;
|
.menu-group-title {font-size: 12px; color: #909399; margin: 8px 6px 10px; text-align: left;}
|
||||||
border: 5px solid #e6e6e6;
|
/* 品牌logo区域 */
|
||||||
border-top: 5px solid #409EFF;
|
.brand-logo-section {display: flex; justify-content: center; align-items: center; width: 100%; height: 60px; margin-bottom: 16px; box-sizing: border-box;}
|
||||||
border-radius: 50%;
|
.brand-logo {width: 100%; height: 100%; object-fit: cover; border-radius: 12px;}
|
||||||
animation: spin 1s linear infinite;
|
.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;}
|
||||||
@keyframes spin {
|
.menu-item.active {background: rgba(0, 0, 0, 0.1) !important; /* color: #409EFF !important; */;}
|
||||||
0% {
|
.menu-text {font-size: 14px;}
|
||||||
transform: rotate(0deg);
|
.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;}
|
||||||
100% {
|
.menu-icon-img {width: 100%; height: 100%; object-fit: contain;}
|
||||||
transform: rotate(360deg);
|
.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;}
|
||||||
.erp-container {
|
.icon-container {display: flex; justify-content: center;}
|
||||||
display: flex;
|
.main-icon {width: 400px; height: 400px; border-radius: 20px; object-fit: contain;}
|
||||||
height: 100vh;
|
.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;}
|
||||||
.sidebar {
|
.placeholder-desc {font-size: 13px; color: #606266;}
|
||||||
width: 180px;
|
.device-dialog-header {display: flex; flex-direction: column; align-items: center; padding: 12px 0 4px 0; margin-left: 40px;}
|
||||||
min-width: 180px;
|
.device-dialog :deep(.el-dialog__header) {text-align: center;}
|
||||||
flex-shrink: 0;
|
.device-dialog :deep(.el-dialog__body) {padding-top: 0;}
|
||||||
background: #F0F0F0;
|
.device-illustration {width: 180px; height: auto; object-fit: contain; margin-bottom: 8px;}
|
||||||
border-right: 1px solid #e8eaec;
|
.device-title {font-size: 18px; font-weight: 600; color: #303133; margin-bottom: 6px;}
|
||||||
padding: 16px 12px;
|
.device-count {color: #909399; font-weight: 500;}
|
||||||
box-sizing: border-box;
|
.device-subtitle {font-size: 12px; color: #909399;}
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar img {
|
|
||||||
width: 90px;
|
|
||||||
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: rgba(0, 0, 0, 0.1) !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-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.device-dialog-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 0 4px 0;
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
|
||||||
.device-dialog :deep(.el-dialog__header) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.device-dialog :deep(.el-dialog__body) { padding-top: 0; }
|
|
||||||
.device-illustration {
|
|
||||||
width: 180px;
|
|
||||||
height: auto;
|
|
||||||
object-fit: contain;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.device-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.device-count { color: #909399; font-weight: 500; }
|
|
||||||
.device-subtitle { font-size: 12px; color: #909399; }
|
|
||||||
|
|
||||||
/* 浮动版本信息 */
|
/* 浮动版本信息 */
|
||||||
.version-info {
|
.version-info {position: fixed; right: 10px; bottom: 10px; background: rgba(255, 255, 255, 0.9); padding: 5px 10px; border-radius: 4px; font-size: 12px; color: #909399; z-index: 1000; cursor: pointer; user-select: none;}
|
||||||
position: fixed;
|
|
||||||
right: 10px;
|
|
||||||
bottom: 10px;
|
|
||||||
background: rgba(255,255,255,0.9);
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
z-index: 1000;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* VIP状态卡片样式 */
|
/* VIP状态卡片样式 */
|
||||||
.vip-status-card {
|
.vip-status-card {margin-top: auto; width: 100%; border-radius: 8px; display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; box-sizing: border-box; background: #BAE0FF; box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15); transition: all 0.3s ease; position: relative; cursor: pointer; user-select: none;}
|
||||||
margin-top: auto;
|
.vip-status-card:hover {box-shadow: 0 3px 10px rgba(255, 215, 0, 0.25); transform: translateY(-1px);}
|
||||||
width: 100%;
|
.vip-status-card:active {transform: translateY(0);}
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #BAE0FF;
|
|
||||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-status-card:hover {
|
|
||||||
box-shadow: 0 3px 10px rgba(255, 215, 0, 0.25);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-status-card:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 正常状态和警告状态 - 统一温暖金色渐变 */
|
/* 正常状态和警告状态 - 统一温暖金色渐变 */
|
||||||
.vip-status-card.vip-active,
|
.vip-status-card.vip-active,
|
||||||
.vip-status-card.vip-normal,
|
.vip-status-card.vip-normal,
|
||||||
.vip-status-card.vip-warning {
|
.vip-status-card.vip-warning {background: #BAE0FF; box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);}
|
||||||
background: #BAE0FF;
|
|
||||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 过期状态 - 灰色,保持水平布局 */
|
/* 过期状态 - 灰色,保持水平布局 */
|
||||||
.vip-status-card.vip-expired {
|
.vip-status-card.vip-expired {background: linear-gradient(135deg, #FAFAFA 0%, #E8E8E8 100%); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);}
|
||||||
background: linear-gradient(135deg, #FAFAFA 0%, #E8E8E8 100%);
|
.vip-info {flex: 1; display: flex; flex-direction: column; gap: 3px; min-width: 0; text-align: left;}
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
.vip-status-text {font-size: 13px; font-weight: 600; color: #001D66; text-align: left; letter-spacing: 0.3px;}
|
||||||
}
|
.vip-expire-date {font-size: 10px; color: #001D66; line-height: 1.3; text-align: left; opacity: 0.9;}
|
||||||
|
.vip-status-card.vip-expired .vip-info {align-items: flex-start;}
|
||||||
.vip-info {
|
.vip-status-card.vip-expired .vip-status-text {color: #909399;}
|
||||||
flex: 1;
|
.vip-status-card.vip-expired .vip-expire-date {color: #B0B0B0;}
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 3px;
|
|
||||||
min-width: 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-status-text {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #001D66;
|
|
||||||
text-align: left;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-expire-date {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #001D66;
|
|
||||||
line-height: 1.3;
|
|
||||||
|
|
||||||
text-align: left;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-status-card.vip-expired .vip-info {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-status-card.vip-expired .vip-status-text {
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-status-card.vip-expired .vip-expire-date {
|
|
||||||
color: #B0B0B0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* 全局样式:限制图片预览器大小 */
|
/* 全局样式:限制图片预览器大小 */
|
||||||
.el-image-viewer__img {
|
.el-image-viewer__img {max-width: 50vw !important; max-height: 50vh !important; width: auto !important; height: auto !important;}
|
||||||
max-width: 50vw !important;
|
|
||||||
max-height: 50vh !important;
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,17 +1,10 @@
|
|||||||
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
import { AppConfig, isRuoyiPath } from '../config'
|
||||||
//const RUOYI_BASE = 'http://8.138.23.49:8085';
|
|
||||||
const RUOYI_BASE = 'http://192.168.1.89:8085';
|
export type HttpMethod = 'GET' | 'POST' | 'DELETE'
|
||||||
export const CONFIG = {
|
export const CONFIG = AppConfig
|
||||||
CLIENT_BASE: 'http://localhost:8081',
|
|
||||||
RUOYI_BASE,
|
|
||||||
SSE_URL: `${RUOYI_BASE}/monitor/account/events`
|
|
||||||
} as const;
|
|
||||||
function resolveBase(path: string): string {
|
function resolveBase(path: string): string {
|
||||||
// 路由到 ruoyi-admin (8085):仅系统管理和监控相关
|
return isRuoyiPath(path) ? CONFIG.RUOYI_BASE : CONFIG.CLIENT_BASE
|
||||||
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai')) {
|
|
||||||
return CONFIG.RUOYI_BASE;
|
|
||||||
}
|
|
||||||
return CONFIG.CLIENT_BASE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildQuery(params?: Record<string, unknown>): string {
|
function buildQuery(params?: Record<string, unknown>): string {
|
||||||
|
|||||||
@@ -2,28 +2,28 @@ import { http } from './http'
|
|||||||
|
|
||||||
export const markApi = {
|
export const markApi = {
|
||||||
// 新建任务(调用 erp_client_sb)
|
// 新建任务(调用 erp_client_sb)
|
||||||
newTask(file: File) {
|
newTask(file: File, signal?: AbortSignal) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
return http.upload<{ code: number, data: any, msg: string }>('/api/trademark/newTask', formData)
|
return http.upload<{ code: number, data: any, msg: string }>('/api/trademark/newTask', formData, signal)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取任务列表及筛选数据(调用 erp_client_sb)
|
// 获取任务列表及筛选数据(调用 erp_client_sb)
|
||||||
getTask() {
|
getTask(signal?: AbortSignal) {
|
||||||
return http.post<{
|
return http.post<{
|
||||||
code: number,
|
code: number,
|
||||||
data: {
|
data: {
|
||||||
original: any,
|
original: any,
|
||||||
filtered: Record<string, any>[], // 完整的行数据(Map格式)
|
filtered: Record<string, any>[], // 完整的行数据(Map格式)
|
||||||
headers: string[] // 表头
|
headers: string[] // 表头
|
||||||
},
|
},
|
||||||
msg: string
|
msg: string
|
||||||
}>('/api/trademark/task')
|
}>('/api/trademark/task', undefined, signal)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 品牌商标筛查
|
// 品牌商标筛查
|
||||||
brandCheck(brands: string[], taskId?: string) {
|
brandCheck(brands: string[], taskId?: string, signal?: AbortSignal) {
|
||||||
return http.post<{ code: number, data: { total: number, checked: number, registered: number, unregistered: number, failed: number, data: any[], duration: string }, msg: string }>('/api/trademark/brandCheck', { brands, taskId })
|
return http.post<{ code: number, data: { total: number, checked: number, registered: number, unregistered: number, failed: number, data: any[], duration: string }, msg: string }>('/api/trademark/brandCheck', { brands, taskId }, signal)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 查询品牌筛查进度
|
// 查询品牌筛查进度
|
||||||
|
|||||||
@@ -27,6 +27,29 @@ export const splashApi = {
|
|||||||
// 删除自定义开屏图片(恢复默认)
|
// 删除自定义开屏图片(恢复默认)
|
||||||
async deleteSplashImage(username: string) {
|
async deleteSplashImage(username: string) {
|
||||||
return http.post<{ data: string }>(`/monitor/account/splash-image/delete?username=${username}`)
|
return http.post<{ data: string }>(`/monitor/account/splash-image/delete?username=${username}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 上传品牌logo
|
||||||
|
async uploadBrandLogo(file: File, username: string) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('username', username)
|
||||||
|
return http.upload<{ data: { url: string; fileName: string } }>('/monitor/account/brand-logo/upload', formData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取当前用户的品牌logo
|
||||||
|
async getBrandLogo(username: string) {
|
||||||
|
return http.get<{ data: { url: string } }>('/monitor/account/brand-logo', { username })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除品牌logo
|
||||||
|
async deleteBrandLogo(username: string) {
|
||||||
|
return http.post<{ data: string }>(`/monitor/account/brand-logo/delete?username=${username}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取全局开屏图片
|
||||||
|
async getGlobalSplashImage() {
|
||||||
|
return http.get<{ data: { url: string } }>('/monitor/account/global-splash-image')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -341,46 +341,32 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.asin-panel {
|
.asin-panel {flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden;}
|
||||||
flex: 1;
|
.steps-flow {position: relative; flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: none;}
|
||||||
min-height: 0;
|
.asin-panel .steps-flow::-webkit-scrollbar {display: none;}
|
||||||
display: flex;
|
.steps-flow:before {content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6);}
|
||||||
flex-direction: column;
|
.flow-item {position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0;}
|
||||||
overflow: hidden;
|
.flow-item .step-index {position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px;}
|
||||||
}
|
.step-card {border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0;}
|
||||||
.steps-flow {
|
.step-header {display: flex; align-items: center; gap: 8px; margin-bottom: 8px;}
|
||||||
position: relative;
|
.title {font-size: 14px; font-weight: 600; color: #303133; text-align: left;}
|
||||||
flex: 1;
|
.desc {font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5;}
|
||||||
min-height: 0;
|
.links {display: flex; align-items: center; gap: 2px; margin-bottom: 8px;}
|
||||||
overflow-y: auto;
|
.link {color: #409EFF; cursor: pointer; font-size: 12px;}
|
||||||
scrollbar-width: none;
|
.sep {color: #dcdfe6;}
|
||||||
}
|
.dropzone {border: 1px dashed #c0c4cc; border-radius: 6px; padding: 16px; text-align: center; cursor: pointer; background: #fafafa;}
|
||||||
.asin-panel .steps-flow::-webkit-scrollbar {
|
.dropzone:hover {background: #f6fbff; border-color: #409EFF;}
|
||||||
display: none;
|
.dz-el-icon {font-size: 18px; margin-bottom: 4px; color: #909399;}
|
||||||
}
|
.dz-text {color: #303133; font-size: 13px;}
|
||||||
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
|
.dz-sub {color: #909399; font-size: 12px;}
|
||||||
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
|
.file-chip {display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box;}
|
||||||
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
.file-chip .dot {width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0;}
|
||||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0; }
|
.file-chip .name {flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
|
||||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
.file-chip .delete-btn {cursor: pointer; opacity: 0.6; flex-shrink: 0;}
|
||||||
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
.file-chip .delete-btn:hover {opacity: 1;}
|
||||||
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
|
.action-buttons.column {display: flex; flex-direction: column; gap: 8px;}
|
||||||
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||||
.link { color: #409EFF; cursor: pointer; font-size: 12px; }
|
.btn-blue:disabled {background: #a6c8ff; border-color: #a6c8ff; color: #fff;}
|
||||||
.sep { color: #dcdfe6; }
|
.w100 {width: 100%;}
|
||||||
.dropzone { border: 1px dashed #c0c4cc; border-radius: 6px; padding: 16px; text-align: center; cursor: pointer; background: #fafafa; }
|
|
||||||
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
|
|
||||||
.dz-el-icon { font-size: 18px; margin-bottom: 4px; color: #909399; }
|
|
||||||
.dz-text { color: #303133; font-size: 13px; }
|
|
||||||
.dz-sub { color: #909399; font-size: 12px; }
|
|
||||||
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box; }
|
|
||||||
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0; }
|
|
||||||
.file-chip .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
.file-chip .delete-btn { cursor: pointer; opacity: 0.6; flex-shrink: 0; }
|
|
||||||
.file-chip .delete-btn:hover { opacity: 1; }
|
|
||||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
|
||||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
|
||||||
.w100 { width: 100%; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -119,55 +119,41 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.genmai-panel {
|
.genmai-panel {flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden;}
|
||||||
flex: 1;
|
.steps-flow {position: relative; flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: none;}
|
||||||
min-height: 0;
|
.genmai-panel .steps-flow::-webkit-scrollbar {display: none;}
|
||||||
display: flex;
|
.steps-flow:before {content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6);}
|
||||||
flex-direction: column;
|
.flow-item {position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0;}
|
||||||
overflow: hidden;
|
.flow-item .step-index {position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px;}
|
||||||
|
.step-card {border: none; border-radius: 0; padding: 0; background: transparent;}
|
||||||
|
.step-header {display: flex; align-items: center; gap: 8px; margin-bottom: 8px;}
|
||||||
|
.title {font-size: 14px; font-weight: 600; color: #303133; text-align: left;}
|
||||||
|
.desc {font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5;}
|
||||||
|
.account-list {height: auto;}
|
||||||
|
.scroll-limit {max-height: 140px;}
|
||||||
|
.placeholder-box {display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 8px;}
|
||||||
|
.placeholder-img {width: 80px; opacity: 0.9;}
|
||||||
|
.placeholder-tip {margin-top: 6px; font-size: 12px; color: #a8abb2;}
|
||||||
|
.avatar {width: 18px; height: 18px; border-radius: 50%;}
|
||||||
|
.acct-row {display: grid; grid-template-columns: 6px 18px 1fr auto; align-items: center; gap: 6px; width: 100%;}
|
||||||
|
.acct-text {overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px;}
|
||||||
|
.status-dot {width: 6px; height: 6px; border-radius: 50%; display: inline-block;}
|
||||||
|
.status-dot.on {background: #22c55e;}
|
||||||
|
.status-dot.off {background: #f87171;}
|
||||||
|
.acct-item {padding: 6px 8px; border-radius: 6px; cursor: pointer; margin-bottom: 4px;}
|
||||||
|
.acct-item.selected {background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff;}
|
||||||
|
.acct-check {display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: transparent; color: #111; font-size: 12px;}
|
||||||
|
.account-list::-webkit-scrollbar {width: 0; height: 0;}
|
||||||
|
.step-actions {margin-top: 8px; display: flex; gap: 8px;}
|
||||||
|
.btn-row {display: grid; grid-template-columns: 1fr 1fr; gap: 8px;}
|
||||||
|
.w50 {width: 100%;}
|
||||||
|
.action-buttons.column {display: flex; flex-direction: column; gap: 8px;}
|
||||||
|
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||||
|
.btn-blue:disabled {background: #a6c8ff; border-color: #a6c8ff; color: #fff;}
|
||||||
|
.w100 {width: 100%;}
|
||||||
|
.inline-spinner {display: inline-block; animation: spin 1s linear infinite;}
|
||||||
|
@keyframes spin {0% { transform: rotate(0deg);}
|
||||||
|
100% {transform: rotate(360deg);}
|
||||||
}
|
}
|
||||||
.steps-flow {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
.genmai-panel .steps-flow::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
|
|
||||||
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
|
|
||||||
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
|
||||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
|
||||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
||||||
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
|
||||||
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
|
|
||||||
|
|
||||||
.account-list { height: auto; }
|
|
||||||
.scroll-limit { max-height: 140px; }
|
|
||||||
.placeholder-box { display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 8px; }
|
|
||||||
.placeholder-img { width: 80px; opacity: 0.9; }
|
|
||||||
.placeholder-tip { margin-top: 6px; font-size: 12px; color: #a8abb2; }
|
|
||||||
.avatar { width: 18px; height: 18px; border-radius: 50%; }
|
|
||||||
.acct-row { display: grid; grid-template-columns: 6px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; }
|
|
||||||
.acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px; }
|
|
||||||
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
|
|
||||||
.status-dot.on { background: #22c55e; }
|
|
||||||
.status-dot.off { background: #f87171; }
|
|
||||||
.acct-item { padding: 6px 8px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; }
|
|
||||||
.acct-item.selected { background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff; }
|
|
||||||
.acct-check { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: transparent; color: #111; font-size: 12px; }
|
|
||||||
.account-list::-webkit-scrollbar { width: 0; height: 0; }
|
|
||||||
|
|
||||||
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
|
|
||||||
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
||||||
.w50 { width: 100%; }
|
|
||||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
|
||||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
|
||||||
.w100 { width: 100%; }
|
|
||||||
.inline-spinner { display: inline-block; animation: spin 1s linear infinite; }
|
|
||||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -35,16 +35,17 @@ async function handleAuth() {
|
|||||||
try {
|
try {
|
||||||
// 获取或生成设备ID
|
// 获取或生成设备ID
|
||||||
const deviceId = await getOrCreateDeviceId()
|
const deviceId = await getOrCreateDeviceId()
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
const loginRes: any = await authApi.login({
|
const loginRes: any = await authApi.login({
|
||||||
...authForm.value,
|
...authForm.value,
|
||||||
clientId: deviceId
|
clientId: deviceId
|
||||||
})
|
})
|
||||||
|
|
||||||
// 保存开屏图片配置(不阻塞登录)
|
// 保存开屏图片配置和品牌logo(不阻塞登录)
|
||||||
saveSplashConfigInBackground(authForm.value.username)
|
saveSplashConfigInBackground(authForm.value.username)
|
||||||
|
saveBrandLogoInBackground(authForm.value.username)
|
||||||
|
|
||||||
emit('loginSuccess', {
|
emit('loginSuccess', {
|
||||||
token: loginRes.data.accessToken || loginRes.data.token,
|
token: loginRes.data.accessToken || loginRes.data.token,
|
||||||
permissions: loginRes.data.permissions,
|
permissions: loginRes.data.permissions,
|
||||||
@@ -90,6 +91,20 @@ async function saveSplashConfigInBackground(username: string) {
|
|||||||
console.error('[开屏图片] 保存配置失败:', error)
|
console.error('[开屏图片] 保存配置失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存品牌logo配置
|
||||||
|
async function saveBrandLogoInBackground(username: string) {
|
||||||
|
try {
|
||||||
|
const res = await splashApi.getBrandLogo(username)
|
||||||
|
const url = res?.data?.url || ''
|
||||||
|
// 保存到本地配置
|
||||||
|
await (window as any).electronAPI.saveBrandLogoConfig(username, url)
|
||||||
|
// 触发App.vue加载品牌logo
|
||||||
|
window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: url }))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[品牌logo] 加载配置失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -151,36 +166,10 @@ async function saveSplashConfigInBackground(username: string) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.auth-logo {
|
.auth-logo {width: 160px; height: auto;}
|
||||||
width: 160px;
|
.auth-dialog {--el-color-primary: #1677FF;}
|
||||||
height: auto;
|
.auth-dialog :deep(.el-button--primary) {background-color: #1677FF; border-color: #1677FF;}
|
||||||
}
|
.auth-title-wrap {margin-bottom: 12px;}
|
||||||
|
.auth-title {margin: 0; font-size: 18px; font-weight: 700; color: #1f1f1f; text-align: left;}
|
||||||
.auth-dialog {
|
.auth-subtitle {margin: 6px 0 0; font-size: 12px; color: #8c8c8c; text-align: left;}
|
||||||
--el-color-primary: #1677FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-dialog :deep(.el-button--primary) {
|
|
||||||
background-color: #1677FF;
|
|
||||||
border-color: #1677FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title-wrap {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1f1f1f;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-subtitle {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -185,36 +185,10 @@ function backToLogin() {
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.auth-logo {
|
.auth-logo {width: 160px; height: auto;}
|
||||||
width: 160px;
|
.auth-dialog {--el-color-primary: #1677FF;}
|
||||||
height: auto;
|
.auth-dialog :deep(.el-button--primary) {background-color: #1677FF; border-color: #1677FF;}
|
||||||
}
|
.auth-title-wrap {margin-bottom: 12px;}
|
||||||
|
.auth-title {margin: 0; font-size: 18px; font-weight: 700; color: #1f1f1f; text-align: left;}
|
||||||
.auth-dialog {
|
.auth-subtitle {margin: 6px 0 0; font-size: 12px; color: #8c8c8c; text-align: left;}
|
||||||
--el-color-primary: #1677FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-dialog :deep(.el-button--primary) {
|
|
||||||
background-color: #1677FF;
|
|
||||||
border-color: #1677FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title-wrap {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1f1f1f;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-subtitle {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -174,46 +174,45 @@ export default defineComponent({ name: 'AccountManager' })
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.acc-manager :deep(.el-dialog__header) { text-align:center; }
|
.acc-manager :deep(.el-dialog__header) {text-align:center;}
|
||||||
.layout { display:grid; grid-template-columns: 160px 1fr; gap: 12px; min-height: 340px; }
|
.layout {display:grid; grid-template-columns: 160px 1fr; gap: 12px; min-height: 340px;}
|
||||||
.sider { border-right: 1px solid #ebeef5; padding-right: 10px; }
|
.sider {border-right: 1px solid #ebeef5; padding-right: 10px;}
|
||||||
.sider-title { color:#303133; font-size:13px; font-weight: 600; margin-bottom: 10px; text-align: left; }
|
.sider-title {color:#303133; font-size:13px; font-weight: 600; margin-bottom: 10px; text-align: left;}
|
||||||
.nav { display:flex; flex-direction: column; gap: 4px; }
|
.nav {display:flex; flex-direction: column; gap: 4px;}
|
||||||
.nav-item { padding: 6px 8px; border-radius: 4px; cursor: pointer; color:#606266; font-size: 12px; transition: all 0.2s; text-align: left; }
|
.nav-item {padding: 6px 8px; border-radius: 4px; cursor: pointer; color:#606266; font-size: 12px; transition: all 0.2s; text-align: left;}
|
||||||
.nav-item:hover { background:#f0f2f5; }
|
.nav-item:hover {background:#f0f2f5;}
|
||||||
.nav-item.active { background:#e6f4ff; color:#409EFF; font-weight: 600; }
|
.nav-item.active {background:#e6f4ff; color:#409EFF; font-weight: 600;}
|
||||||
.platform-bar { font-weight: 600; color:#303133; margin: 0 0 12px 0; text-align: left; font-size: 14px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; }
|
.platform-bar {font-weight: 600; color:#303133; margin: 0 0 12px 0; text-align: left; font-size: 14px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5;}
|
||||||
.content { display:flex; flex-direction: column; min-width: 0; }
|
.content {display:flex; flex-direction: column; min-width: 0;}
|
||||||
.top { display:flex; flex-direction: column; align-items:center; gap: 6px; margin-bottom: 12px; }
|
.top {display:flex; flex-direction: column; align-items:center; gap: 6px; margin-bottom: 12px;}
|
||||||
.hero { width: 160px; height: auto; }
|
.hero {width: 160px; height: auto;}
|
||||||
.head-main { text-align:center; }
|
.head-main {text-align:center;}
|
||||||
.main-title { font-size: 16px; font-weight: 600; color:#303133; margin-bottom: 4px; }
|
.main-title {font-size: 16px; font-weight: 600; color:#303133; margin-bottom: 4px;}
|
||||||
.main-sub { color:#909399; font-size: 11px; line-height: 1.4; }
|
.main-sub {color:#909399; font-size: 11px; line-height: 1.4;}
|
||||||
.upgrade { color:#409EFF; cursor: pointer; font-weight: 600; transition: all 0.2s ease; }
|
.upgrade {color:#409EFF; cursor: pointer; font-weight: 600; transition: all 0.2s ease;}
|
||||||
.upgrade:hover { color:#0d5ed6; text-decoration: underline; }
|
.upgrade:hover {color:#0d5ed6; text-decoration: underline;}
|
||||||
.list { border:1px solid #ebeef5; border-radius: 6px; background: #fff; flex: 0 0 auto; width: 100%; max-height: 160px; overflow-y: auto; }
|
.list {border:1px solid #ebeef5; border-radius: 6px; background: #fff; flex: 0 0 auto; width: 100%; max-height: 160px; overflow-y: auto;}
|
||||||
.list.compact { max-height: 48px; }
|
.list.compact {max-height: 48px;}
|
||||||
|
|
||||||
/* 添加账号对话框样式 */
|
/* 添加账号对话框样式 */
|
||||||
.add-account-dialog .aad-header { display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%; }
|
.add-account-dialog .aad-header {display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%;}
|
||||||
.add-account-dialog .aad-icon { width: 120px; height: auto; }
|
.add-account-dialog .aad-icon {width: 120px; height: auto;}
|
||||||
.add-account-dialog .aad-title { font-weight: 600; font-size: 18px; text-align: center; }
|
.add-account-dialog .aad-title {font-weight: 600; font-size: 18px; text-align: center;}
|
||||||
.add-account-dialog .aad-row { margin-top: 12px; }
|
.add-account-dialog .aad-row {margin-top: 12px;}
|
||||||
:deep(.add-account-dialog .el-dialog__header) { text-align: center; padding-right: 0; display: block; }
|
:deep(.add-account-dialog .el-dialog__header) {text-align: center; padding-right: 0; display: block;}
|
||||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||||
.btn-blue:hover { background: #0d5ed6; border-color: #0d5ed6; }
|
.btn-blue:hover {background: #0d5ed6; border-color: #0d5ed6;}
|
||||||
.row { display:grid; grid-template-columns: 8px 1fr 120px 60px; gap: 8px; align-items:center; padding: 4px 8px; border-bottom: 1px solid #f5f5f5; height: 28px; }
|
.row {display:grid; grid-template-columns: 8px 1fr 120px 60px; gap: 8px; align-items:center; padding: 4px 8px; border-bottom: 1px solid #f5f5f5; height: 28px;}
|
||||||
.row:last-child { border-bottom:none; }
|
.row:last-child {border-bottom:none;}
|
||||||
.row:hover { background:#fafafa; }
|
.row:hover {background:#fafafa;}
|
||||||
.dot { width:6px; height:6px; border-radius:50%; justify-self: center; }
|
.dot {width:6px; height:6px; border-radius:50%; justify-self: center;}
|
||||||
.dot.on { background:#52c41a; }
|
.dot.on {background:#52c41a;}
|
||||||
.dot.off { background:#ff4d4f; }
|
.dot.off {background:#ff4d4f;}
|
||||||
.user-info { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
.user-info {display: flex; align-items: center; gap: 8px; min-width: 0;}
|
||||||
.avatar { width:22px; height:22px; border-radius:50%; object-fit: cover; }
|
.avatar {width:22px; height:22px; border-radius:50%; object-fit: cover;}
|
||||||
.name { font-weight:500; font-size: 13px; color:#303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.name {font-weight:500; font-size: 13px; color:#303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
|
||||||
.date { color:#999; font-size:11px; text-align: center; }
|
.date {color:#999; font-size:11px; text-align: center;}
|
||||||
.footer { display:flex; justify-content:center; padding-top: 10px; }
|
.footer {display:flex; justify-content:center; padding-top: 10px;}
|
||||||
.btn { width: 180px; height: 32px; font-size: 13px; }
|
.btn {width: 180px; height: 32px; font-size: 13px;}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function handleConfirm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function copyWechat() {
|
function copyWechat() {
|
||||||
navigator.clipboard.writeText('_linhong').then(() => {
|
navigator.clipboard.writeText('butaihaoba001').then(() => {
|
||||||
ElMessage.success('微信号已复制')
|
ElMessage.success('微信号已复制')
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
ElMessage.error('复制失败,请手动复制')
|
ElMessage.error('复制失败,请手动复制')
|
||||||
@@ -75,7 +75,7 @@ function copyWechat() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="wechat-info">
|
<div class="wechat-info">
|
||||||
<div class="wechat-label">客服微信</div>
|
<div class="wechat-label">客服微信</div>
|
||||||
<div class="wechat-id">_linhong</div>
|
<div class="wechat-id">butaihaoba001</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copy-icon">📋</div>
|
<div class="copy-icon">📋</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,116 +93,23 @@ function copyWechat() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.trial-expired-dialog :deep(.el-dialog) {
|
.trial-expired-dialog :deep(.el-dialog) {border-radius: 16px;}
|
||||||
border-radius: 16px;
|
.trial-expired-dialog :deep(.el-dialog__header) {padding: 0; margin: 0;}
|
||||||
}
|
.trial-expired-dialog :deep(.el-dialog__body) {padding: 20px;}
|
||||||
|
.expired-content {display: flex; flex-direction: column; align-items: center; padding: 10px 0;}
|
||||||
.trial-expired-dialog :deep(.el-dialog__header) {
|
.expired-logo {width: 160px; height: auto;}
|
||||||
padding: 0;
|
.expired-title {font-size: 18px; font-weight: 700; color: #1f1f1f; margin: 0 0 8px 0; text-align: center;}
|
||||||
margin: 0;
|
.expired-subtitle {font-size: 12px; color: #8c8c8c; margin: 0 0 20px 0; text-align: center; line-height: 1.5;}
|
||||||
}
|
.wechat-card {display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: #f5f5f5; border-radius: 6px; margin-bottom: 20px; width: 90%; cursor: pointer; transition: all 0.3s; position: relative;}
|
||||||
|
.wechat-card:hover {background: #e8f5e9; box-shadow: 0 2px 8px rgba(9, 187, 7, 0.15);}
|
||||||
.trial-expired-dialog :deep(.el-dialog__body) {
|
.wechat-icon {flex-shrink: 0;}
|
||||||
padding: 20px;
|
.wechat-icon svg {width: 36px; height: 36px;}
|
||||||
}
|
.wechat-info {flex: 1; text-align: left;}
|
||||||
|
.wechat-label {font-size: 12px; color: #666; margin-bottom: 2px;}
|
||||||
.expired-content {
|
.wechat-id {font-size: 15px; font-weight: 500; color: #1f1f1f;}
|
||||||
display: flex;
|
.copy-icon {margin-left: auto; font-size: 16px; opacity: 0.5; transition: all 0.3s;}
|
||||||
flex-direction: column;
|
.wechat-card:hover .copy-icon {opacity: 1; transform: scale(1.1);}
|
||||||
align-items: center;
|
.confirm-btn {height: 40px; font-size: 14px; font-weight: 500; background: #1677FF; border-color: #1677FF; border-radius: 6px;}
|
||||||
padding: 10px 0;
|
.confirm-btn:hover {background: #4096ff; border-color: #4096ff;}
|
||||||
}
|
|
||||||
|
|
||||||
.expired-logo {
|
|
||||||
width: 160px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expired-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1f1f1f;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expired-subtitle {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
width: 90%;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-card:hover {
|
|
||||||
background: #e8f5e9;
|
|
||||||
box-shadow: 0 2px 8px rgba(9, 187, 7, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-icon svg {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-info {
|
|
||||||
flex: 1;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-id {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1f1f1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-icon {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 16px;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-card:hover .copy-icon {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn {
|
|
||||||
height: 40px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: #1677FF;
|
|
||||||
border-color: #1677FF;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn:hover {
|
|
||||||
background: #4096ff;
|
|
||||||
border-color: #4096ff;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -298,292 +298,58 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.update-dialog .el-dialog) {
|
:deep(.update-dialog .el-dialog) {border-radius: 16px; box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);}
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 通用标题样式 */
|
/* 通用标题样式 */
|
||||||
:deep(.update-dialog .el-dialog__title) {
|
:deep(.update-dialog .el-dialog__title) {font-size: 14px; font-weight: 500; margin-left: 8px;}
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 默认标题样式(第一阶段 - 检查阶段) */
|
/* 默认标题样式(第一阶段 - 检查阶段) */
|
||||||
:deep(.update-dialog.stage-check .el-dialog__header) {
|
:deep(.update-dialog.stage-check .el-dialog__header) {display: block; text-align: left;}
|
||||||
display: block;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 第二阶段 - 下载中,标题居中 */
|
/* 第二阶段 - 下载中,标题居中 */
|
||||||
:deep(.update-dialog.stage-downloading .el-dialog__header) {
|
:deep(.update-dialog.stage-downloading .el-dialog__header) {display: block; text-align: center;}
|
||||||
display: block;
|
:deep(.update-dialog.stage-downloading .el-dialog__title) {margin-left: 20px;}
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.update-dialog.stage-downloading .el-dialog__title) {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 第三阶段 - 下载完成,标题居中 */
|
/* 第三阶段 - 下载完成,标题居中 */
|
||||||
:deep(.update-dialog.stage-completed .el-dialog__header) {
|
:deep(.update-dialog.stage-completed .el-dialog__header) {display: block; text-align: center;}
|
||||||
display: block;
|
:deep(.update-dialog.stage-completed .el-dialog__title) {margin-left: 20px;}
|
||||||
text-align: center;
|
:deep(.update-dialog .el-dialog__body) {padding: 0;}
|
||||||
}
|
.update-content {text-align: left;}
|
||||||
|
.update-layout {display: grid; grid-template-columns: 88px 1fr; align-items: start; margin-bottom: 5px;}
|
||||||
:deep(.update-dialog.stage-completed .el-dialog__title) {
|
.left-pane {display: flex; flex-direction: column; align-items: flex-start;}
|
||||||
margin-left: 20px;
|
.app-icon-large {width: 70px; height: 70px; border-radius: 12px; margin: 4px 0 0 0;}
|
||||||
}
|
.right-pane {min-width: 0;}
|
||||||
|
.right-pane .announce {font-size: 16px; font-weight: 600; color: #1f2937; margin: 4px 0 6px; word-break: break-word;}
|
||||||
:deep(.update-dialog .el-dialog__body) {
|
.right-pane .desc {font-size: 13px; color: #6b7280; line-height: 1.6; margin: 0; word-break: break-word;}
|
||||||
padding: 0;
|
.update-header {display: flex; align-items: flex-start; margin-bottom: 24px;}
|
||||||
}
|
.update-header.text-center {text-align: center; flex-direction: column; align-items: center;}
|
||||||
|
.app-icon {width: 70px; height: 70px; border-radius: 12px; margin-right: 16px; flex-shrink: 0;}
|
||||||
.update-content {
|
.update-header.text-center .app-icon {margin-right: 0; margin-bottom: 16px;}
|
||||||
text-align: left;
|
.update-header h3 {font-size: 20px; font-weight: 600; margin: 16px 0 8px 0; color: #1f2937;}
|
||||||
}
|
.update-header p {font-size: 14px; color: #6b7280; margin: 0; line-height: 1.5;}
|
||||||
|
.update-details {border-radius: 8px; padding: 0; margin: 12px 0 8px 0;}
|
||||||
.update-layout {
|
.update-details.form {max-height: none;}
|
||||||
display: grid;
|
.notes-box :deep(textarea.el-textarea__inner) {white-space: pre-wrap;}
|
||||||
grid-template-columns: 88px 1fr;
|
.update-details h4 {font-size: 14px; font-weight: 600; color: #374151; margin: 0 0 8px 0;}
|
||||||
align-items: start;
|
.update-actions.row {display: flex; flex-direction: column; align-items: stretch; gap: 12px;}
|
||||||
margin-bottom: 5px;
|
.update-buttons {display: flex; justify-content: space-between; gap: 12px;}
|
||||||
}
|
.update-actions.row .update-buttons {justify-content: space-between;}
|
||||||
|
:deep(.update-actions.row .update-buttons .el-button) {flex: none; min-width: 100px;}
|
||||||
.left-pane {
|
.left-actions {display: flex; gap: 12px;}
|
||||||
display: flex;
|
.right-actions {display: flex; gap: 8px;}
|
||||||
flex-direction: column;
|
:deep(.update-buttons .el-button) {flex: 1; height: 32px; font-size: 13px; border-radius: 8px;}
|
||||||
align-items: flex-start;
|
.download-header h3 {font-size: 14px; font-weight: 500; margin: 0; color: #1f2937;}
|
||||||
}
|
.download-main {display: grid; grid-template-columns: 80px 1fr; align-items: start;}
|
||||||
|
.download-icon {display: flex; justify-content: center;}
|
||||||
.app-icon-large {
|
.download-icon .app-icon {width: 64px; height: 64px; border-radius: 12px;}
|
||||||
width: 70px;
|
.download-content {min-width: 0;}
|
||||||
height: 70px;
|
.download-info {margin-bottom: 12px;}
|
||||||
border-radius: 12px;
|
.download-info p {font-size: 14px; font-weight: 600; color: #6b7280; margin: 0;}
|
||||||
margin: 4px 0 0 0;
|
.download-progress {margin: 0;}
|
||||||
}
|
.progress-info {display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 14px; color: #6b7280;}
|
||||||
|
.progress-details {margin-top: 12px; display: flex; justify-content: space-between; align-items: center;}
|
||||||
.right-pane {
|
.progress-details span {font-size: 12px; color: #909399;}
|
||||||
min-width: 0;
|
.action-buttons {display: flex; gap: 8px;}
|
||||||
}
|
:deep(.el-progress-bar__outer) {border-radius: 4px; background-color: #e5e7eb;}
|
||||||
|
:deep(.el-progress-bar__inner) {border-radius: 4px; transition: width 0.3s ease;}
|
||||||
.right-pane .announce {
|
:deep(.update-buttons .el-button--primary) {background-color: #2563eb; border-color: #2563eb; font-weight: 500;}
|
||||||
font-size: 16px;
|
:deep(.update-buttons .el-button--primary:hover) {background-color: #1d4ed8; border-color: #1d4ed8;}
|
||||||
font-weight: 600;
|
:deep(.update-buttons .el-button:not(.el-button--primary)) {background-color: #f3f4f6; border-color: #d1d5db; color: #374151; font-weight: 500;}
|
||||||
color: #1f2937;
|
:deep(.update-buttons .el-button:not(.el-button--primary):hover) {background-color: #e5e7eb; border-color: #9ca3af;}
|
||||||
margin: 4px 0 6px;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-pane .desc {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #6b7280;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-header.text-center {
|
|
||||||
text-align: center;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-icon {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin-right: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-header.text-center .app-icon {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-header h3 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 16px 0 8px 0;
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-header p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #6b7280;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-details {
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0;
|
|
||||||
margin: 12px 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-details.form {
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notes-box :deep(textarea.el-textarea__inner) {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-details h4 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #374151;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-actions.row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-actions.row .update-buttons {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.update-actions.row .update-buttons .el-button) {
|
|
||||||
flex: none;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.update-buttons .el-button) {
|
|
||||||
flex: 1;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.download-header h3 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin: 0;
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-main {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 80px 1fr;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-icon {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-icon .app-icon {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-content {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-info {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-info p {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6b7280;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-progress {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-info {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-details {
|
|
||||||
margin-top: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-details span {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-progress-bar__outer) {
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-progress-bar__inner) {
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.update-buttons .el-button--primary) {
|
|
||||||
background-color: #2563eb;
|
|
||||||
border-color: #2563eb;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.update-buttons .el-button--primary:hover) {
|
|
||||||
background-color: #1d4ed8;
|
|
||||||
border-color: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.update-buttons .el-button:not(.el-button--primary)) {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
border-color: #d1d5db;
|
|
||||||
color: #374151;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.update-buttons .el-button:not(.el-button--primary):hover) {
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
border-color: #9ca3af;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -20,7 +20,6 @@ interface Emits {
|
|||||||
(e: 'open-settings'): void
|
(e: 'open-settings'): void
|
||||||
(e: 'open-account-manager'): void
|
(e: 'open-account-manager'): void
|
||||||
(e: 'check-update'): void
|
(e: 'check-update'): void
|
||||||
(e: 'show-login'): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -113,10 +112,6 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
|
|
||||||
<!-- 登录/注册按钮 -->
|
|
||||||
<button v-if="!isAuthenticated" class="login-btn" @click="$emit('show-login')">
|
|
||||||
登录/注册
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-center">
|
<div class="navbar-center">
|
||||||
<div class="breadcrumbs">
|
<div class="breadcrumbs">
|
||||||
@@ -145,250 +140,44 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.top-navbar {
|
.top-navbar {height: 40px; 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); -webkit-app-region: drag; user-select: none;}
|
||||||
height: 40px;
|
.navbar-left {display: flex; align-items: center; gap: 8px; flex: 0 0 auto; -webkit-app-region: no-drag;}
|
||||||
display: flex;
|
.navbar-center {display: flex; justify-content: center; flex: 1;}
|
||||||
align-items: center;
|
.navbar-right {display: flex; align-items: center; gap: 8px; flex: 0 0 auto; -webkit-app-region: no-drag;}
|
||||||
justify-content: space-between;
|
.nav-controls {display: flex; gap: 4px;}
|
||||||
padding: 0 16px;
|
.nav-btn {width: 28px; height: 28px; border: none; background: transparent; cursor: pointer; font-size: 16px; color: #606266; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; outline: none; border-radius: 4px;}
|
||||||
background: #ffffff;
|
.arrow-icon {width: 18px; height: 18px; flex-shrink: 0;}
|
||||||
border-bottom: 1px solid #e8eaec;
|
.arrow-icon path {stroke: currentColor;}
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
.nav-btn:hover:not(:disabled) {background: rgba(0, 0, 0, 0.05); color: #409EFF;}
|
||||||
-webkit-app-region: drag;
|
.nav-btn:hover:not(:disabled) .arrow-icon path {stroke: #409EFF;}
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #606266;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
outline: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-icon {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-icon path {
|
|
||||||
stroke: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:hover:not(:disabled) {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
color: #409EFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:hover:not(:disabled) .arrow-icon path {
|
|
||||||
stroke: #409EFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:focus,
|
.nav-btn:focus,
|
||||||
.nav-btn:active {
|
.nav-btn:active {outline: none; border: none;}
|
||||||
outline: none;
|
.nav-btn:disabled {cursor: not-allowed; background: transparent; color: #d0d0d0;}
|
||||||
border: none;
|
.nav-btn:disabled .arrow-icon path {stroke: #d0d0d0;}
|
||||||
}
|
.nav-btn-round {width: 28px; height: 28px; border: none; border-radius: 4px; background: transparent; cursor: pointer; font-size: 16px; color: #606266; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; outline: none;}
|
||||||
|
.nav-btn-round:hover {background: rgba(0, 0, 0, 0.05); color: #409EFF;}
|
||||||
.nav-btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
background: transparent;
|
|
||||||
color: #d0d0d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:disabled .arrow-icon path {
|
|
||||||
stroke: #d0d0d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn-round {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #606266;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn-round:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
color: #409EFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn-round:focus,
|
.nav-btn-round:focus,
|
||||||
.nav-btn-round:active {
|
.nav-btn-round:active {outline: none;}
|
||||||
outline: none;
|
.breadcrumbs {display: flex; align-items: center; color: #606266; font-size: 14px;}
|
||||||
}
|
.separator {margin: 0 6px; color: #c0c4cc; font-size: 14px;}
|
||||||
|
|
||||||
.breadcrumbs {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #606266;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
margin: 0 6px;
|
|
||||||
color: #c0c4cc;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 窗口控制按钮 */
|
/* 窗口控制按钮 */
|
||||||
.window-controls {
|
.window-controls {display: flex; align-items: center; gap: 4px; margin-left: 8px;}
|
||||||
display: flex;
|
.window-btn {width: 32px; height: 28px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #606266; transition: all 0.2s ease; outline: none; padding: 0; margin: 0; border-radius: 4px;}
|
||||||
align-items: center;
|
.window-btn:hover {background: rgba(0, 0, 0, 0.05);}
|
||||||
gap: 4px;
|
.window-btn:active {background: rgba(0, 0, 0, 0.1);}
|
||||||
margin-left: 8px;
|
.window-btn-close:hover {background: #e81123; color: #ffffff;}
|
||||||
}
|
.window-btn-close:active {background: #f1707a;}
|
||||||
|
.maximize-icon {width: 12px; height: 12px;}
|
||||||
.window-btn {
|
|
||||||
width: 32px;
|
|
||||||
height: 28px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
outline: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-btn:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-btn:active {
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-btn-close:hover {
|
|
||||||
background: #e81123;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-btn-close:active {
|
|
||||||
background: #f1707a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maximize-icon {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 登录/注册按钮 */
|
/* 登录/注册按钮 */
|
||||||
.login-btn {
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #1677FF;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
outline: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn:hover {
|
|
||||||
background: #0d5ed6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn:active {
|
|
||||||
background: #0c54c2;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* 设置下拉菜单样式 */
|
/* 设置下拉菜单样式 */
|
||||||
.settings-dropdown {
|
.settings-dropdown {min-width: 180px !important; padding: 4px 0 !important; border-radius: 12px !important; margin-top: 4px !important;}
|
||||||
min-width: 180px !important;
|
.settings-dropdown .username-item {font-weight: 600 !important; color: #000000 !important; cursor: default !important; padding: 8px 16px !important; font-size: 14px !important;}
|
||||||
padding: 4px 0 !important;
|
.settings-dropdown .menu-item {padding: 8px 16px !important; font-size: 13px !important; color: #000000 !important; transition: all 0.2s ease !important;}
|
||||||
border-radius: 12px !important;
|
.settings-dropdown .menu-item:hover {background: #f5f7fa !important; color: #409EFF !important;}
|
||||||
margin-top: 4px !important;
|
.settings-dropdown .logout-item {color: #000000 !important;}
|
||||||
}
|
.settings-dropdown .logout-item:hover {background: #f5f7fa !important; color: #409EFF !important;}
|
||||||
|
.settings-dropdown .el-dropdown-menu__item.is-disabled {cursor: default !important; opacity: 1 !important;}
|
||||||
.settings-dropdown .username-item {
|
|
||||||
font-weight: 600 !important;
|
|
||||||
color: #000000 !important;
|
|
||||||
cursor: default !important;
|
|
||||||
padding: 8px 16px !important;
|
|
||||||
font-size: 14px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-dropdown .menu-item {
|
|
||||||
padding: 8px 16px !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
color: #000000 !important;
|
|
||||||
transition: all 0.2s ease !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-dropdown .menu-item:hover {
|
|
||||||
background: #f5f7fa !important;
|
|
||||||
color: #409EFF !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-dropdown .logout-item {
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-dropdown .logout-item:hover {
|
|
||||||
background: #f5f7fa !important;
|
|
||||||
color: #409EFF !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-dropdown .el-dropdown-menu__item.is-disabled {
|
|
||||||
cursor: default !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -649,229 +649,101 @@ onMounted(loadLatest)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rakuten-root {
|
.rakuten-root {position: absolute; inset: 0; background: #fff; box-sizing: border-box;}
|
||||||
position: absolute;
|
.main-container {height: 100%; display: flex; flex-direction: column; padding: 12px; box-sizing: border-box;}
|
||||||
inset: 0;
|
.body-layout {display: flex; gap: 12px; height: 100%;}
|
||||||
background: #fff;
|
.steps-sidebar {width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0;}
|
||||||
box-sizing: border-box;
|
.steps-title {font-size: 14px; font-weight: 600; color: #303133; text-align: left;}
|
||||||
}
|
|
||||||
|
|
||||||
.main-container {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-layout { display: flex; gap: 12px; height: 100%; }
|
|
||||||
.steps-sidebar { width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0; }
|
|
||||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
|
||||||
|
|
||||||
/* 卡片式步骤,与示例一致 */
|
/* 卡片式步骤,与示例一致 */
|
||||||
.steps-flow { position: relative; }
|
.steps-flow {position: relative;}
|
||||||
.steps-flow:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); }
|
.steps-flow:before {content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6);}
|
||||||
.flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; }
|
.flow-item {position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0;}
|
||||||
.flow-item .step-index { position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
.flow-item .step-index {position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px;}
|
||||||
.flow-item:after { display: none; }
|
.flow-item:after {display: none;}
|
||||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0; }
|
.step-card {border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0;}
|
||||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
.step-header {display: flex; align-items: center; gap: 8px; margin-bottom: 6px;}
|
||||||
.title { font-size: 13px; font-weight: 600; color: #303133; text-align: left; }
|
.title {font-size: 13px; font-weight: 600; color: #303133; text-align: left;}
|
||||||
.desc { font-size: 12px; color: #909399; margin-bottom: 8px; text-align: left; }
|
.desc {font-size: 12px; color: #909399; margin-bottom: 8px; text-align: left;}
|
||||||
.mini-hint { font-size: 12px; color: #909399; margin-top: 8px; text-align: left; }
|
.mini-hint {font-size: 12px; color: #909399; margin-top: 8px; text-align: left;}
|
||||||
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
.links {display: flex; align-items: center; gap: 6px; margin-bottom: 8px;}
|
||||||
.link { color: #409EFF; cursor: pointer; font-size: 12px; }
|
.link {color: #409EFF; cursor: pointer; font-size: 12px;}
|
||||||
.sep { color: #dcdfe6; }
|
.sep {color: #dcdfe6;}
|
||||||
|
.content-panel {flex: 1; display: flex; flex-direction: column; min-width: 0;}
|
||||||
.content-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
.left-controls {margin-top: 10px; display: flex; flex-direction: column; gap: 10px;}
|
||||||
|
.dropzone {border: 1px dashed #c0c4cc; border-radius: 6px; padding: 12px; text-align: center; cursor: pointer; background: #fafafa;}
|
||||||
.left-controls { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; }
|
.dropzone:hover {background: #f6fbff; border-color: #409EFF;}
|
||||||
.dropzone { border: 1px dashed #c0c4cc; border-radius: 6px; padding: 12px; text-align: center; cursor: pointer; background: #fafafa; }
|
.dropzone.disabled {opacity: .6; cursor: not-allowed;}
|
||||||
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
|
.dz-el-icon {font-size: 18px; margin-bottom: 4px; color: #909399;}
|
||||||
.dropzone.disabled { opacity: .6; cursor: not-allowed; }
|
.dz-text {color: #303133; font-size: 13px;}
|
||||||
.dz-el-icon { font-size: 18px; margin-bottom: 4px; color: #909399; }
|
.dz-sub {color: #909399; font-size: 12px;}
|
||||||
.dz-text { color: #303133; font-size: 13px; }
|
.single-input.left {display: flex; gap: 8px;}
|
||||||
.dz-sub { color: #909399; font-size: 12px; }
|
.action-buttons.column {display: flex; flex-direction: column; gap: 8px;}
|
||||||
.single-input.left { display: flex; gap: 8px; }
|
.file-chip {display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box;}
|
||||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
.file-chip .dot {width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0;}
|
||||||
|
.file-chip .name {flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
|
||||||
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box; }
|
.file-chip .delete-btn {cursor: pointer; opacity: 0.6; flex-shrink: 0;}
|
||||||
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0; }
|
.file-chip .delete-btn:hover {opacity: 1;}
|
||||||
.file-chip .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.progress-section.left {margin-top: 10px;}
|
||||||
.file-chip .delete-btn { cursor: pointer; opacity: 0.6; flex-shrink: 0; }
|
.full {width: 100%;}
|
||||||
.file-chip .delete-btn:hover { opacity: 1; }
|
.form-row {margin-bottom: 10px;}
|
||||||
|
.label {display: block; font-size: 12px; color: #606266; margin-bottom: 6px;}
|
||||||
.progress-section.left { margin-top: 10px; }
|
|
||||||
.full { width: 100%; }
|
|
||||||
.form-row { margin-bottom: 10px; }
|
|
||||||
.label { display: block; font-size: 12px; color: #606266; margin-bottom: 6px; }
|
|
||||||
|
|
||||||
/* 统一左侧控件宽度与主色 */
|
/* 统一左侧控件宽度与主色 */
|
||||||
.steps-sidebar :deep(.el-date-editor),
|
.steps-sidebar :deep(.el-date-editor),
|
||||||
.steps-sidebar :deep(.el-range-editor.el-input__wrapper),
|
.steps-sidebar :deep(.el-range-editor.el-input__wrapper),
|
||||||
.steps-sidebar :deep(.el-input),
|
.steps-sidebar :deep(.el-input),
|
||||||
.steps-sidebar :deep(.el-input__wrapper),
|
.steps-sidebar :deep(.el-input__wrapper),
|
||||||
.steps-sidebar :deep(.el-select) { width: 100%; box-sizing: border-box; }
|
.steps-sidebar :deep(.el-select) {width: 100%; box-sizing: border-box;}
|
||||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
.btn-blue:disabled {background: #a6c8ff; border-color: #a6c8ff; color: #fff;}
|
||||||
.w100 { width: 100%; }
|
.w100 {width: 100%;}
|
||||||
.steps-sidebar :deep(.el-button + .el-button) { margin-left: 0; }
|
.steps-sidebar :deep(.el-button + .el-button) {margin-left: 0;}
|
||||||
.progress-section { margin: 0px 12px 0px 12px; }
|
.progress-section {margin: 0px 12px 0px 12px;}
|
||||||
.progress-box { padding: 4px 0; }
|
.progress-box {padding: 4px 0;}
|
||||||
.progress-container { display: flex; align-items: center; gap: 8px; }
|
.progress-container {display: flex; align-items: center; gap: 8px;}
|
||||||
.progress-bar { flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
|
.progress-bar {flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden;}
|
||||||
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
|
.progress-fill {height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease;}
|
||||||
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
|
.progress-text {font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right;}
|
||||||
|
.current-status {font-size: 12px; color: #606266; padding-left: 2px;}
|
||||||
.current-status {
|
.export-progress {display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px;}
|
||||||
font-size: 12px;
|
.export-progress-bar {flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden;}
|
||||||
color: #606266;
|
.export-progress-fill {height: 100%; background: #1677FF; border-radius: 2px; transition: width 0.3s ease;}
|
||||||
padding-left: 2px;
|
.export-progress-text {font-size: 11px; color: #1677FF; font-weight: 500; min-width: 32px; text-align: right;}
|
||||||
}
|
.table-container {display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden;}
|
||||||
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
|
.empty-section {flex: 1; display: flex; justify-content: center; align-items: center; background: #fff; border: 1px solid #ebeef5; border-radius: 6px;}
|
||||||
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
|
.empty-container {text-align: center;}
|
||||||
.export-progress-fill { height: 100%; background: #1677FF; border-radius: 2px; transition: width 0.3s ease; }
|
.empty-icon {font-size: 48px; margin-bottom: 16px; opacity: 0.6;}
|
||||||
.export-progress-text { font-size: 11px; color: #1677FF; font-weight: 500; min-width: 32px; text-align: right; }
|
.empty-text {font-size: 14px; color: #909399;}
|
||||||
|
.table-section {flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column;}
|
||||||
.table-container {
|
.table-wrapper {flex: 1; overflow: auto;}
|
||||||
display: flex;
|
.table-wrapper {scrollbar-width: thin; scrollbar-color: #c0c4cc transparent;}
|
||||||
flex-direction: column;
|
.table-wrapper::-webkit-scrollbar {width: 6px; height: 6px;}
|
||||||
flex: 1;
|
.table-wrapper::-webkit-scrollbar-track {background: transparent;}
|
||||||
min-height: 400px;
|
.table-wrapper::-webkit-scrollbar-thumb {background: #c0c4cc; border-radius: 3px;}
|
||||||
overflow: hidden;
|
.table-wrapper:hover::-webkit-scrollbar-thumb {background: #a8abb2;}
|
||||||
}
|
.table {width: max-content; min-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;}
|
||||||
.empty-section {
|
.table td {padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle;}
|
||||||
flex: 1;
|
.table tbody tr:hover {background: #f9f9f9;}
|
||||||
display: flex;
|
.truncate {max-width: 260px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
|
||||||
justify-content: center;
|
.shop-col {max-width: 160px;}
|
||||||
align-items: center;
|
.url-col {max-width: 220px;}
|
||||||
background: #fff;
|
.empty-tip {text-align: center; color: #909399; padding: 16px 0;}
|
||||||
border: 1px solid #ebeef5;
|
.empty-container {text-align: center;}
|
||||||
border-radius: 6px;
|
.empty-icon {font-size: 48px; margin-bottom: 12px; opacity: 0.6;}
|
||||||
}
|
.empty-text {font-size: 14px; color: #909399;}
|
||||||
|
.import-section.drag-active {border: 1px dashed #409EFF; border-radius: 6px;}
|
||||||
.empty-container {
|
.empty-abs {position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none;}
|
||||||
text-align: center;
|
.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; pointer-events: none;}
|
||||||
.empty-icon {
|
.spinner {font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px;}
|
||||||
font-size: 48px;
|
@keyframes spin {0% {
|
||||||
margin-bottom: 16px;
|
transform: rotate(0deg);}
|
||||||
opacity: 0.6;
|
100% {transform: rotate(360deg);}
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
|
|
||||||
.table-wrapper { flex: 1; overflow: auto; }
|
|
||||||
.table-wrapper { scrollbar-width: thin; scrollbar-color: #c0c4cc transparent; }
|
|
||||||
.table-wrapper::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
||||||
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
|
|
||||||
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
|
|
||||||
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
|
|
||||||
.table { width: max-content; min-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; }
|
|
||||||
.empty-container { text-align: center; }
|
|
||||||
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.6; }
|
|
||||||
.empty-text { font-size: 14px; color: #909399; }
|
|
||||||
.import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
|
|
||||||
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
|
||||||
|
|
||||||
.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;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 0 12px;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-fixed :deep(.el-pager li.is-active) {
|
|
||||||
border: 1px solid #1677FF;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #1677FF;
|
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-fixed {flex-shrink: 0; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end;}
|
||||||
|
.pagination-fixed :deep(.el-pager li.is-active) {border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff;}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -575,93 +575,94 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.zebra-root { position: absolute; inset: 0; background: #fff; box-sizing: border-box; }
|
.zebra-root {position: absolute; inset: 0; background: #fff; box-sizing: border-box;}
|
||||||
.layout { height: 100%; display: grid; grid-template-columns: 220px 1fr; gap: 12px; padding: 12px; box-sizing: border-box; }
|
.layout {height: 100%; display: grid; grid-template-columns: 220px 1fr; gap: 12px; padding: 12px; box-sizing: border-box;}
|
||||||
.aside { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; transition: width 0.2s ease; }
|
.aside {border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; transition: width 0.2s ease;}
|
||||||
.aside.collapsed { width: 56px; overflow: hidden; }
|
.aside.collapsed {width: 56px; overflow: hidden;}
|
||||||
.aside-header { display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; }
|
.aside-header {display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px;}
|
||||||
.aside-steps { position: relative; }
|
.aside-steps {position: relative;}
|
||||||
.step { display: grid; grid-template-columns: 22px 1fr; gap: 10px; position: relative; padding: 8px 0; }
|
.step {display: grid; grid-template-columns: 22px 1fr; gap: 10px; position: relative; padding: 8px 0;}
|
||||||
.step-index { width: 22px; height: 22px; background: #1677FF; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
.step-index {width: 22px; height: 22px; background: #1677FF; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; margin-top: 2px;}
|
||||||
.step-body { min-width: 0; text-align: left; }
|
.step-body {min-width: 0; text-align: left;}
|
||||||
.step-title { font-size: 13px; color: #606266; margin-bottom: 6px; font-weight: 600; text-align: left; }
|
.step-title {font-size: 13px; color: #606266; margin-bottom: 6px; font-weight: 600; text-align: left;}
|
||||||
.aside-steps:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); }
|
.aside-steps:before {content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6);}
|
||||||
.account-list {height: auto; }
|
.account-list {height: auto;}
|
||||||
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
|
.step-actions {margin-top: 8px; display: flex; gap: 8px;}
|
||||||
.step-accounts { position: relative; }
|
.step-accounts {position: relative;}
|
||||||
.sticky-actions { position: sticky; bottom: 0; background: #fafafa; padding-top: 8px; }
|
.sticky-actions {position: sticky; bottom: 0; background: #fafafa; padding-top: 8px;}
|
||||||
.scroll-limit { max-height: 160px; }
|
.scroll-limit {max-height: 160px;}
|
||||||
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
.btn-row {display: grid; grid-template-columns: 1fr 1fr; gap: 8px;}
|
||||||
.btn-col { display: flex; flex-direction: column; gap: 6px; }
|
.btn-col {display: flex; flex-direction: column; gap: 6px;}
|
||||||
.w50 { width: 48%; }
|
.w50 {width: 48%;}
|
||||||
.w100 { width: 100%; }
|
.w100 {width: 100%;}
|
||||||
.placeholder-box { display:flex; align-items:center; justify-content:center; flex-direction:column; height: 140px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
.placeholder-box {display:flex; align-items:center; justify-content:center; flex-direction:column; height: 140px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px;}
|
||||||
.placeholder-img { width: 120px; opacity: 0.9; }
|
.placeholder-img {width: 120px; opacity: 0.9;}
|
||||||
.placeholder-tip { margin-top: 6px; font-size: 12px; color: #a8abb2; }
|
.placeholder-tip {margin-top: 6px; font-size: 12px; color: #a8abb2;}
|
||||||
.aside :deep(.el-date-editor) { width: 100%; }
|
.aside :deep(.el-date-editor) {width: 100%;}
|
||||||
.aside :deep(.el-range-editor.el-input__wrapper) { width: 100%; box-sizing: border-box; }
|
.aside :deep(.el-range-editor.el-input__wrapper) {width: 100%; box-sizing: border-box;}
|
||||||
.aside :deep(.el-input),
|
.aside :deep(.el-input),
|
||||||
.aside :deep(.el-input__wrapper),
|
.aside :deep(.el-input__wrapper),
|
||||||
.aside :deep(.el-select) { width: 100%; box-sizing: border-box; }
|
.aside :deep(.el-select) {width: 100%; box-sizing: border-box;}
|
||||||
.aside :deep(.el-button + .el-button) { margin-left: 0 !important; }
|
.aside :deep(.el-button + .el-button) {margin-left: 0 !important;}
|
||||||
.btn-row :deep(.el-button) { width: 100%; }
|
.btn-row :deep(.el-button) {width: 100%;}
|
||||||
.btn-col :deep(.el-button) { width: 100%; }
|
.btn-col :deep(.el-button) {width: 100%;}
|
||||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
.btn-blue:disabled {background: #a6c8ff; border-color: #a6c8ff; color: #fff;}
|
||||||
.tip { color: #909399; font-size: 12px; margin-bottom: 8px; text-align: left; }
|
.tip {color: #909399; font-size: 12px; margin-bottom: 8px; text-align: left;}
|
||||||
.avatar { width: 22px; height: 22px; border-radius: 50%; margin-right: 6px; vertical-align: -2px; }
|
.avatar {width: 22px; height: 22px; border-radius: 50%; margin-right: 6px; vertical-align: -2px;}
|
||||||
.acct-text { vertical-align: middle; }
|
.acct-text {vertical-align: middle;}
|
||||||
.acct-row { display: grid; grid-template-columns: 8px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; }
|
.acct-row {display: grid; grid-template-columns: 8px 18px 1fr auto; align-items: center; gap: 6px; width: 100%;}
|
||||||
.acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px; }
|
.acct-text {overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px;}
|
||||||
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
|
.status-dot {width: 6px; height: 6px; border-radius: 50%; display: inline-block;}
|
||||||
.status-dot.on { background: #22c55e; }
|
.status-dot.on {background: #22c55e;}
|
||||||
.status-dot.off { background: #f87171; }
|
.status-dot.off {background: #f87171;}
|
||||||
.acct-item { padding: 6px 8px; border-radius: 8px; cursor: pointer; }
|
.acct-item {padding: 6px 8px; border-radius: 8px; cursor: pointer;}
|
||||||
.acct-item.selected { background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff; }
|
.acct-item.selected {background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff;}
|
||||||
.acct-check { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; background: transparent; color: #111; font-size: 14px; }
|
.acct-check {display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; background: transparent; color: #111; font-size: 14px;}
|
||||||
.account-list::-webkit-scrollbar { width: 0; height: 0; }
|
.account-list::-webkit-scrollbar {width: 0; height: 0;}
|
||||||
.add-account-dialog .aad-header { display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%; }
|
.add-account-dialog .aad-header {display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%;}
|
||||||
.add-account-dialog .aad-icon { width: 120px; height: auto; }
|
.add-account-dialog .aad-icon {width: 120px; height: auto;}
|
||||||
.add-account-dialog .aad-title { font-weight: 600; font-size: 18px; text-align: center; }
|
.add-account-dialog .aad-title {font-weight: 600; font-size: 18px; text-align: center;}
|
||||||
.add-account-dialog .aad-row { margin-top: 12px; }
|
.add-account-dialog .aad-row {margin-top: 12px;}
|
||||||
.add-account-dialog .aad-opts { display:flex; align-items:center; }
|
.add-account-dialog .aad-opts {display:flex; align-items:center;}
|
||||||
|
|
||||||
/* 居中 header,避免右上角关闭按钮影响视觉中心 */
|
/* 居中 header,避免右上角关闭按钮影响视觉中心 */
|
||||||
:deep(.add-account-dialog .el-dialog__header) { text-align: center; padding-right: 0; display: block; }
|
:deep(.add-account-dialog .el-dialog__header) {text-align: center; padding-right: 0; display: block;}
|
||||||
.content { display: grid; grid-template-rows: 1fr auto; min-height: 0; }
|
.content {display: grid; grid-template-rows: 1fr auto; min-height: 0;}
|
||||||
.table-section { min-height: 0; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
|
.table-section {min-height: 0; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column;}
|
||||||
.table-wrapper { flex: 1; overflow: auto; overflow-x: auto; }
|
.table-wrapper {flex: 1; overflow: auto; overflow-x: auto;}
|
||||||
.table-wrapper { scrollbar-width: thin; scrollbar-color: #c0c4cc transparent; }
|
.table-wrapper {scrollbar-width: thin; scrollbar-color: #c0c4cc transparent;}
|
||||||
.table-wrapper::-webkit-scrollbar { width: 6px; height: 6px; }
|
.table-wrapper::-webkit-scrollbar {width: 6px; height: 6px;}
|
||||||
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
|
.table-wrapper::-webkit-scrollbar-track {background: transparent;}
|
||||||
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
|
.table-wrapper::-webkit-scrollbar-thumb {background: #c0c4cc; border-radius: 3px;}
|
||||||
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
|
.table-wrapper:hover::-webkit-scrollbar-thumb {background: #a8abb2;}
|
||||||
.table { width: max-content; min-width: 100%; border-collapse: collapse; font-size: 13px; }
|
.table {width: max-content; min-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; white-space: nowrap; }
|
.table th {background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; white-space: nowrap;}
|
||||||
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
|
.table td {padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle;}
|
||||||
.table tbody tr:hover { background: #f9f9f9; }
|
.table tbody tr:hover {background: #f9f9f9;}
|
||||||
.truncate { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.truncate {max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
|
||||||
.image-container { display: flex; justify-content: center; align-items: center; width: 28px; height: 24px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
|
.image-container {display: flex; justify-content: center; align-items: center; width: 28px; height: 24px; margin: 0 auto; background: #f8f9fa; border-radius: 2px;}
|
||||||
.thumb { width: 22px; height: 22px; object-fit: contain; border-radius: 2px; }
|
.thumb {width: 22px; height: 22px; object-fit: contain; border-radius: 2px;}
|
||||||
.price-tag { color: #e6a23c; font-weight: bold; }
|
.price-tag {color: #e6a23c; font-weight: bold;}
|
||||||
.fee-tag { color: #909399; font-weight: 500; }
|
.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; }
|
.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; }
|
.spinner {font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px;}
|
||||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
@keyframes spin {0% { transform: rotate(0deg);}
|
||||||
.pagination-fixed { position: sticky; bottom: 0; z-index: 2; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end; }
|
100% {transform: rotate(360deg);}
|
||||||
.pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; }
|
}
|
||||||
.tag { display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
|
.pagination-fixed {position: sticky; bottom: 0; z-index: 2; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end;}
|
||||||
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
.pagination-fixed :deep(.el-pager li.is-active) {border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff;}
|
||||||
.progress-section { margin: 0px 12px 0px 12px; }
|
.tag {display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px;}
|
||||||
.progress-box { padding: 4px 0; }
|
.empty-abs {position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none;}
|
||||||
.progress-container { display: flex; align-items: center; gap: 8px; }
|
.progress-section {margin: 0px 12px 0px 12px;}
|
||||||
.progress-bar { flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
|
.progress-box {padding: 4px 0;}
|
||||||
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
|
.progress-container {display: flex; align-items: center; gap: 8px;}
|
||||||
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
|
.progress-bar {flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden;}
|
||||||
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
|
.progress-fill {height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease;}
|
||||||
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
|
.progress-text {font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right;}
|
||||||
.export-progress-fill { height: 100%; background: #67c23a; border-radius: 2px; transition: width 0.3s ease; }
|
.export-progress {display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px;}
|
||||||
.export-progress-text { font-size: 11px; color: #67c23a; font-weight: 500; min-width: 32px; text-align: right; }
|
.export-progress-bar {flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden;}
|
||||||
|
.export-progress-fill {height: 100%; background: #67c23a; border-radius: 2px; transition: width 0.3s ease;}
|
||||||
|
.export-progress-text {font-size: 11px; color: #67c23a; font-weight: 500; min-width: 32px; text-align: right;}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
20
electron-vue-template/src/renderer/config/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* 应用配置
|
||||||
|
*/
|
||||||
|
export const AppConfig = {
|
||||||
|
CLIENT_BASE: 'http://localhost:8081',
|
||||||
|
RUOYI_BASE: 'http://8.138.23.49:8085',
|
||||||
|
get SSE_URL() {
|
||||||
|
return `${this.RUOYI_BASE}/monitor/account/events`
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断路径是否路由到ruoyi-admin服务
|
||||||
|
*/
|
||||||
|
export function isRuoyiPath(path: string): boolean {
|
||||||
|
return path.startsWith('/monitor/') ||
|
||||||
|
path.startsWith('/system/') ||
|
||||||
|
path.startsWith('/tool/banma') ||
|
||||||
|
path.startsWith('/tool/genmai')
|
||||||
|
}
|
||||||
177
electron-vue-template/src/renderer/utils/imageCompressor.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* 图片压缩工具 - 在上传前压缩图片,减少传输和存储大小
|
||||||
|
* 保持视觉效果的同时显著减小文件体积
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CompressOptions {
|
||||||
|
/** 目标质量 0-1,默认0.8 */
|
||||||
|
quality?: number
|
||||||
|
/** 最大宽度,超过则等比缩放,默认1920 */
|
||||||
|
maxWidth?: number
|
||||||
|
/** 最大高度,超过则等比缩放,默认1080 */
|
||||||
|
maxHeight?: number
|
||||||
|
/** 输出格式,默认'image/jpeg' */
|
||||||
|
mimeType?: string
|
||||||
|
/** 是否转换为WebP格式(更小体积),默认false */
|
||||||
|
useWebP?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩图片文件
|
||||||
|
* @param file 原始图片文件
|
||||||
|
* @param options 压缩选项
|
||||||
|
* @returns 压缩后的Blob和压缩信息
|
||||||
|
*/
|
||||||
|
export async function compressImage(
|
||||||
|
file: File,
|
||||||
|
options: CompressOptions = {}
|
||||||
|
): Promise<{
|
||||||
|
blob: Blob
|
||||||
|
file: File
|
||||||
|
originalSize: number
|
||||||
|
compressedSize: number
|
||||||
|
compressionRatio: number
|
||||||
|
}> {
|
||||||
|
const {
|
||||||
|
quality = 0.85,
|
||||||
|
maxWidth = 1920,
|
||||||
|
maxHeight = 1080,
|
||||||
|
mimeType = 'image/jpeg',
|
||||||
|
useWebP = false,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
// 计算压缩后的尺寸(保持宽高比)
|
||||||
|
let { width, height } = img
|
||||||
|
const aspectRatio = width / height
|
||||||
|
|
||||||
|
if (width > maxWidth) {
|
||||||
|
width = maxWidth
|
||||||
|
height = width / aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height > maxHeight) {
|
||||||
|
height = maxHeight
|
||||||
|
width = height * aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建canvas进行压缩
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error('无法获取canvas上下文'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制图片
|
||||||
|
ctx.drawImage(img, 0, 0, width, height)
|
||||||
|
|
||||||
|
// 转换为Blob - 如果原图是PNG,保持PNG格式以保留透明度
|
||||||
|
const isPNG = file.type === 'image/png'
|
||||||
|
const outputMimeType = useWebP ? 'image/webp' : (isPNG ? 'image/png' : mimeType)
|
||||||
|
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
reject(new Error('图片压缩失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalSize = file.size
|
||||||
|
const compressedSize = blob.size
|
||||||
|
const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2)
|
||||||
|
|
||||||
|
// 转换为File对象
|
||||||
|
const isPNG = file.type === 'image/png'
|
||||||
|
let newFileName = file.name
|
||||||
|
if (useWebP) {
|
||||||
|
newFileName = file.name.replace(/\.[^.]+$/, '.webp')
|
||||||
|
} else if (!isPNG) {
|
||||||
|
newFileName = file.name.replace(/\.[^.]+$/, '.jpg')
|
||||||
|
}
|
||||||
|
// 如果是PNG,保持原文件名
|
||||||
|
|
||||||
|
const compressedFile = new File(
|
||||||
|
[blob],
|
||||||
|
newFileName,
|
||||||
|
{ type: outputMimeType }
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
blob,
|
||||||
|
file: compressedFile,
|
||||||
|
originalSize,
|
||||||
|
compressedSize,
|
||||||
|
compressionRatio: parseFloat(compressionRatio),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
outputMimeType,
|
||||||
|
isPNG ? 1.0 : quality // PNG使用无损压缩(quality=1.0)
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(new Error('图片加载失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = e.target?.result as string
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error('文件读取失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小
|
||||||
|
*/
|
||||||
|
export function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预设压缩配置
|
||||||
|
*/
|
||||||
|
export const COMPRESS_PRESETS = {
|
||||||
|
/** 高质量(适合开屏图片、品牌Logo)- 1920x1080, 85%质量 */
|
||||||
|
HIGH: {
|
||||||
|
quality: 0.85,
|
||||||
|
maxWidth: 1920,
|
||||||
|
maxHeight: 1080,
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
},
|
||||||
|
/** 中等质量(适合一般图片)- 1280x720, 80%质量 */
|
||||||
|
MEDIUM: {
|
||||||
|
quality: 0.8,
|
||||||
|
maxWidth: 1280,
|
||||||
|
maxHeight: 720,
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
},
|
||||||
|
/** 缩略图(适合列表展示)- 400x400, 75%质量 */
|
||||||
|
THUMBNAIL: {
|
||||||
|
quality: 0.75,
|
||||||
|
maxWidth: 400,
|
||||||
|
maxHeight: 400,
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
@@ -35,3 +35,15 @@ export function getClientIdFromToken(token?: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRegisterTimeFromToken(token?: string): string {
|
||||||
|
try {
|
||||||
|
const t = token || getToken();
|
||||||
|
const payload = JSON.parse(atob(t.split('.')[1]));
|
||||||
|
if (!payload.registerTime) return '';
|
||||||
|
const date = new Date(payload.registerTime);
|
||||||
|
return date.toISOString();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</parent>
|
</parent>
|
||||||
<groupId>com.tashow.erp</groupId>
|
<groupId>com.tashow.erp</groupId>
|
||||||
<artifactId>erp_client_sb</artifactId>
|
<artifactId>erp_client_sb</artifactId>
|
||||||
<version>2.6.2</version>
|
<version>2.6.3</version>
|
||||||
<name>erp_client_sb</name>
|
<name>erp_client_sb</name>
|
||||||
<description>erp客户端</description>
|
<description>erp客户端</description>
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.tashow.erp.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1688业务常量
|
||||||
|
*/
|
||||||
|
public class Alibaba1688Constants {
|
||||||
|
public static final String APP_ID = "32517";
|
||||||
|
public static final String INTERFACE_NAME = "imageOfferSearchService";
|
||||||
|
public static final String APP_NAME = "ios";
|
||||||
|
public static final String SEARCH_SCENE = "image";
|
||||||
|
public static final String SEO_SCENE = "seoSearch";
|
||||||
|
public static final int PAGE_SIZE = 40;
|
||||||
|
public static final String JSV_VERSION = "2.6.1";
|
||||||
|
public static final String API_VERSION = "2.0";
|
||||||
|
public static final String DATA_TYPE = "json";
|
||||||
|
public static final int TIMEOUT_MS = 10000;
|
||||||
|
public static final String API_BASE = "https://h5api.m.1688.com/h5";
|
||||||
|
public static final String API_METHOD = "mtop.relationrecommend.WirelessRecommend.recommend";
|
||||||
|
|
||||||
|
private Alibaba1688Constants() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.tashow.erp.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 亚马逊业务常量
|
||||||
|
*/
|
||||||
|
public class AmazonConstants {
|
||||||
|
public static final String REGION_JP = "JP";
|
||||||
|
public static final String REGION_US = "US";
|
||||||
|
public static final String DOMAIN_JP = "https://www.amazon.co.jp";
|
||||||
|
public static final String DOMAIN_US = "https://www.amazon.com";
|
||||||
|
public static final String URL_PRODUCT_PATH = "/dp/";
|
||||||
|
public static final String SESSION_PREFIX = "SINGLE_";
|
||||||
|
public static final String DATA_TYPE = "AMAZON";
|
||||||
|
public static final int RETRY_TIMES = 3;
|
||||||
|
public static final int SLEEP_TIME_BASE = 2000;
|
||||||
|
public static final int SLEEP_TIME_RANDOM = 2000;
|
||||||
|
public static final int TIMEOUT_MS = 20000;
|
||||||
|
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||||
|
public static final String HEADER_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8";
|
||||||
|
public static final String HEADER_ACCEPT_ENCODING = "gzip, deflate, br";
|
||||||
|
public static final String HEADER_ACCEPT_LANGUAGE_US = "zh-CN,zh;q=0.9,en;q=0.8";
|
||||||
|
public static final String HEADER_ACCEPT_LANGUAGE_JP = "ja,en;q=0.9,zh-CN;q=0.8";
|
||||||
|
public static final String HEADER_CACHE_CONTROL = "max-age=0";
|
||||||
|
public static final String COOKIE_I18N_PREFS_US = "USD";
|
||||||
|
public static final String COOKIE_I18N_PREFS_JP = "JPY";
|
||||||
|
public static final String COOKIE_LC_US = "en_US";
|
||||||
|
public static final String COOKIE_LC_JP = "zh_CN";
|
||||||
|
public static final String COOKIE_SESSION_ID_US = "134-6097934-2082600";
|
||||||
|
public static final String COOKIE_SESSION_ID_JP = "358-1261309-0483141";
|
||||||
|
public static final String COOKIE_SESSION_ID_TIME = "2082787201l";
|
||||||
|
public static final String COOKIE_UBID_US = "132-7547587-3056927";
|
||||||
|
public static final String COOKIE_UBID_JP = "357-8224002-9668932";
|
||||||
|
public static final String COOKIE_SKIN = "noskin";
|
||||||
|
|
||||||
|
private AmazonConstants() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.tashow.erp.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 斑马业务常量
|
||||||
|
*/
|
||||||
|
public class BanmaConstants {
|
||||||
|
public static final String API_BASE = "https://banma365.cn";
|
||||||
|
public static final String API_ORDER_LIST = API_BASE + "/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d";
|
||||||
|
public static final String API_ORDER_LIST_WITH_TIME = API_BASE + "/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d";
|
||||||
|
public static final String API_TRACKING = API_BASE + "/zebraExpressHub/web/tracking/getByExpressNumber/%s";
|
||||||
|
public static final String API_SHOP_LIST = API_BASE + "/api/shop/list?_t=%d";
|
||||||
|
public static final int CONNECT_TIMEOUT_SECONDS = 5;
|
||||||
|
public static final int READ_TIMEOUT_SECONDS = 10;
|
||||||
|
public static final String DATA_TYPE = "BANMA";
|
||||||
|
public static final String DATA_TYPE_CACHE = "BANMA_CACHE";
|
||||||
|
public static final String TRACKING_PREFIX_ORDER = "ORDER_";
|
||||||
|
public static final String TRACKING_PREFIX_PRODUCT = "PRODUCT_";
|
||||||
|
public static final String TRACKING_PREFIX_UNKNOWN = "UNKNOWN_";
|
||||||
|
public static final String SESSION_PREFIX = "SESSION_";
|
||||||
|
public static final int CACHE_HOURS = 1;
|
||||||
|
|
||||||
|
private BanmaConstants() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.tashow.erp.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存相关常量
|
||||||
|
*/
|
||||||
|
public class CacheConstants {
|
||||||
|
public static final int DATA_RETENTION_HOURS = 1;
|
||||||
|
public static final int TRADEMARK_CACHE_DAYS = 1;
|
||||||
|
public static final int SESSION_LIMIT = 1;
|
||||||
|
public static final int RAKUTEN_CACHE_HOURS = 1;
|
||||||
|
|
||||||
|
private CacheConstants() {}
|
||||||
|
}
|
||||||
@@ -1,169 +1,16 @@
|
|||||||
package com.tashow.erp.common;
|
package com.tashow.erp.common;
|
||||||
|
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用常量信息
|
* 通用常量(保留兼容性)
|
||||||
*
|
* 新代码请使用具体业务常量类:AmazonConstants、RakutenConstants、HttpConstants等
|
||||||
* @author ruoyi
|
|
||||||
*/
|
*/
|
||||||
public class Constants
|
@Deprecated
|
||||||
{
|
public class Constants {
|
||||||
/**
|
public static final String HTTP = HttpConstants.HTTP;
|
||||||
* UTF-8 字符集
|
public static final String HTTPS = HttpConstants.HTTPS;
|
||||||
*/
|
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
|
||||||
public static final String UTF8 = "UTF-8";
|
|
||||||
|
|
||||||
/**
|
private Constants() {}
|
||||||
* 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" };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.tashow.erp.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方舟精选业务常量
|
||||||
|
*/
|
||||||
|
public class FangzhouConstants {
|
||||||
|
public static final String API_URL = "https://api.fangzhoujingxuan.com/Task";
|
||||||
|
public static final String API_SECRET = "e10adc3949ba59abbe56e057f20f883e";
|
||||||
|
public static final int TOKEN_EXPIRED_CODE = -1006;
|
||||||
|
public static final String WEBSITE_CODE = "1";
|
||||||
|
public static final int SUCCESS_CODE = 1;
|
||||||
|
|
||||||
|
private FangzhouConstants() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.tashow.erp.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP协议常量
|
||||||
|
*/
|
||||||
|
public class HttpConstants {
|
||||||
|
public static final String HTTP = "http://";
|
||||||
|
public static final String HTTPS = "https://";
|
||||||
|
public static final String CHARSET_UTF8 = "UTF-8";
|
||||||
|
public static final String CHARSET_GBK = "GBK";
|
||||||
|
|
||||||
|
private HttpConstants() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.tashow.erp.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐天业务常量
|
||||||
|
*/
|
||||||
|
public class RakutenConstants {
|
||||||
|
public static final String DATA_TYPE = "RAKUTEN";
|
||||||
|
public static final String DOMAIN = "https://item.rakuten.co.jp";
|
||||||
|
public static final int RETRY_TIMES = 3;
|
||||||
|
public static final int SLEEP_TIME_BASE = 2000;
|
||||||
|
public static final int SLEEP_TIME_RANDOM = 2000;
|
||||||
|
public static final int TIMEOUT_MS = 20000;
|
||||||
|
public static final String DATA_TYPE_CACHE = "RAKUTEN_CACHE";
|
||||||
|
|
||||||
|
private RakutenConstants() {}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
package com.tashow.erp.controller;
|
package com.tashow.erp.controller;
|
||||||
|
|
||||||
|
import com.tashow.erp.common.RakutenConstants;
|
||||||
import com.tashow.erp.model.RakutenProduct;
|
import com.tashow.erp.model.RakutenProduct;
|
||||||
import com.tashow.erp.model.SearchResult;
|
import com.tashow.erp.model.SearchResult;
|
||||||
import com.tashow.erp.service.Alibaba1688Service;
|
import com.tashow.erp.service.Alibaba1688Service;
|
||||||
@@ -14,6 +16,7 @@ import org.springframework.util.CollectionUtils;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +84,7 @@ public class RakutenController {
|
|||||||
}
|
}
|
||||||
int cachedCount = allProducts.size() - newProducts.size();
|
int cachedCount = allProducts.size() - newProducts.size();
|
||||||
if (cachedCount > 0) {
|
if (cachedCount > 0) {
|
||||||
dataReportUtil.reportDataCollection("RAKUTEN_CACHE", cachedCount, "0");
|
dataReportUtil.reportDataCollection(RakutenConstants.DATA_TYPE_CACHE, cachedCount, "0");
|
||||||
}
|
}
|
||||||
return JsonData.buildSuccess(Map.of(
|
return JsonData.buildSuccess(Map.of(
|
||||||
"products", allProducts,
|
"products", allProducts,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package com.tashow.erp.controller;
|
package com.tashow.erp.controller;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.tashow.erp.entity.TrademarkSessionEntity;
|
import com.tashow.erp.entity.TrademarkSessionEntity;
|
||||||
@@ -8,6 +9,7 @@ import com.tashow.erp.service.IFangzhouApiService;
|
|||||||
import com.tashow.erp.utils.ExcelParseUtil;
|
import com.tashow.erp.utils.ExcelParseUtil;
|
||||||
import com.tashow.erp.utils.JsonData;
|
import com.tashow.erp.utils.JsonData;
|
||||||
import com.tashow.erp.utils.LoggerUtil;
|
import com.tashow.erp.utils.LoggerUtil;
|
||||||
|
import com.tashow.erp.utils.ProxyPool;
|
||||||
import com.tashow.erp.utils.TrademarkCheckUtil;
|
import com.tashow.erp.utils.TrademarkCheckUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
@@ -17,11 +19,15 @@ import org.slf4j.Logger;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 商标检查控制器 - 极速版(浏览器内并发)
|
* 商标检查控制器
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/trademark")
|
@RequestMapping("/api/trademark")
|
||||||
@@ -29,87 +35,186 @@ import java.util.stream.Collectors;
|
|||||||
public class TrademarkController {
|
public class TrademarkController {
|
||||||
private static final Logger logger = LoggerUtil.getLogger(TrademarkController.class);
|
private static final Logger logger = LoggerUtil.getLogger(TrademarkController.class);
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TrademarkCheckUtil util;
|
private ProxyPool proxyPool;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BrandTrademarkCacheService cacheService;
|
private BrandTrademarkCacheService cacheService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TrademarkSessionRepository sessionRepository;
|
private TrademarkSessionRepository sessionRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IFangzhouApiService fangzhouApi;
|
private IFangzhouApiService fangzhouApi;
|
||||||
|
|
||||||
// 进度追踪
|
|
||||||
private final Map<String, Integer> progressMap = new java.util.concurrent.ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
// 任务取消标志
|
|
||||||
private final Map<String, Boolean> cancelMap = new java.util.concurrent.ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/**
|
private static final Map<String, Integer> progressMap = new ConcurrentHashMap<>();
|
||||||
* 批量品牌商标筛查
|
private static final Map<String, Boolean> cancelMap = new ConcurrentHashMap<>();
|
||||||
*/
|
private static final Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<String, java.util.concurrent.ExecutorService> taskExecutors = new ConcurrentHashMap<>();
|
||||||
|
private static volatile String currentTaskId = null;
|
||||||
|
private static final Object taskLock = new Object();
|
||||||
|
private static volatile boolean isUploadingFile = false;
|
||||||
|
private static final Object uploadLock = new Object();
|
||||||
|
|
||||||
|
@GetMapping("/progress/{taskId}")
|
||||||
|
public SseEmitter getProgress(@PathVariable String taskId) {
|
||||||
|
SseEmitter emitter = new SseEmitter(300000L);
|
||||||
|
sseEmitters.put(taskId, emitter);
|
||||||
|
emitter.onCompletion(() -> sseEmitters.remove(taskId));
|
||||||
|
emitter.onTimeout(() -> sseEmitters.remove(taskId));
|
||||||
|
emitter.onError((e) -> sseEmitters.remove(taskId));
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/brandCheck")
|
@PostMapping("/brandCheck")
|
||||||
public JsonData brandCheck(@RequestBody Map<String, Object> request) {
|
public JsonData brandCheck(@RequestBody Map<String, Object> request) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<String> brands = (List<String>) request.get("brands");
|
List<String> brands = (List<String>) request.get("brands");
|
||||||
String taskId = (String) request.get("taskId");
|
String taskId = (String) request.get("taskId");
|
||||||
|
|
||||||
|
synchronized (taskLock) {
|
||||||
|
if (currentTaskId != null && !currentTaskId.equals(taskId)) {
|
||||||
|
logger.info("检测到新任务 {},终止旧任务 {}", taskId, currentTaskId);
|
||||||
|
forceTerminateTask(currentTaskId);
|
||||||
|
}
|
||||||
|
currentTaskId = taskId;
|
||||||
|
cancelMap.remove(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 保持与前端传入数量一致,不去重(允许重复品牌以匹配产品数量)
|
|
||||||
List<String> list = brands.stream()
|
List<String> list = brands.stream()
|
||||||
.filter(b -> b != null && !b.trim().isEmpty())
|
.filter(b -> !b.trim().isEmpty())
|
||||||
.map(String::trim)
|
.map(String::trim)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
// 1. 先从全局缓存获取
|
|
||||||
Map<String, Boolean> cached = cacheService.getCached(list);
|
Map<String, Boolean> cached = cacheService.getCached(list);
|
||||||
|
|
||||||
// 2. 找出缓存未命中的品牌
|
|
||||||
List<String> toQuery = list.stream()
|
List<String> toQuery = list.stream()
|
||||||
.filter(b -> !cached.containsKey(b))
|
.filter(b -> !cached.containsKey(b))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
logger.info("全局缓存命中: {}/{},需查询: {}", cached.size(), list.size(), toQuery.size());
|
Map<String, Boolean> queried = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
// 3. 查询未命中的品牌
|
|
||||||
Map<String, Boolean> queried = new HashMap<>();
|
|
||||||
if (!toQuery.isEmpty()) {
|
if (!toQuery.isEmpty()) {
|
||||||
for (int i = 0; i < toQuery.size(); i++) {
|
List<List<String>> chunks = new ArrayList<>();
|
||||||
// 检查任务是否被取消值
|
int totalBrands = toQuery.size();
|
||||||
if (taskId != null && cancelMap.getOrDefault(taskId, false)) {
|
if (totalBrands <= 100) {
|
||||||
logger.info("任务 {} 已被取消,停止查询", taskId);
|
chunks.add(toQuery);
|
||||||
break;
|
} else {
|
||||||
}
|
int chunkSize = 100;
|
||||||
|
int numChunks = (totalBrands + chunkSize - 1) / chunkSize;
|
||||||
String brand = toQuery.get(i);
|
int baseSize = totalBrands / numChunks;
|
||||||
logger.info("处理第 {} 个: {}", i + 1, brand);
|
int remainder = totalBrands % numChunks;
|
||||||
|
|
||||||
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand), queried);
|
int startIndex = 0;
|
||||||
queried.putAll(results);
|
for (int i = 0; i < numChunks; i++) {
|
||||||
|
int currentChunkSize = baseSize + (i < remainder ? 1 : 0);
|
||||||
// 更新进度
|
chunks.add(toQuery.subList(startIndex, startIndex + currentChunkSize));
|
||||||
if (taskId != null) {
|
startIndex += currentChunkSize;
|
||||||
progressMap.put(taskId, cached.size() + queried.size());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询结束,保存所有品牌
|
// 根据实际线程数获取代理,不浪费
|
||||||
if (!queried.isEmpty())
|
int proxyCount = chunks.size();
|
||||||
cacheService.saveResults(queried);
|
List<String> proxies = proxyPool.getProxies(proxyCount);
|
||||||
|
if (proxies.size() < chunks.size()) {
|
||||||
|
logger.warn("代理数量不足,需要{}个,实际获取{}个", chunks.size(), proxies.size());
|
||||||
|
}
|
||||||
|
logger.info("获取到{}个代理,分配给{}个线程", proxies.size(), chunks.size());
|
||||||
|
|
||||||
|
java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(chunks.size());
|
||||||
|
taskExecutors.put(taskId, executor);
|
||||||
|
List<java.util.concurrent.Future<Map<String, Boolean>>> futures = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < chunks.size(); i++) {
|
||||||
|
if (cancelMap.getOrDefault(taskId, false)) {
|
||||||
|
logger.info("任务 {} 已被取消", taskId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> chunk = chunks.get(i);
|
||||||
|
String proxy = proxies.isEmpty() ? null : proxies.get(i % proxies.size());
|
||||||
|
|
||||||
|
final int chunkIndex = i;
|
||||||
|
futures.add(executor.submit(() -> {
|
||||||
|
if (cancelMap.getOrDefault(taskId, false)) {
|
||||||
|
return new HashMap<String, Boolean>();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("线程 {} 开始处理 {} 个品牌,使用代理: {}", chunkIndex, chunk.size(), proxy);
|
||||||
|
Map<String, Boolean> result = TrademarkCheckUtil.batchCheck(chunk, proxy, taskId, cancelMap, chunkIndex, sseEmitters);
|
||||||
|
|
||||||
|
if (cancelMap.getOrDefault(taskId, false)) {
|
||||||
|
return new HashMap<String, Boolean>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (java.util.concurrent.Future<Map<String, Boolean>> future : futures) {
|
||||||
|
if (cancelMap.getOrDefault(taskId, false)) {
|
||||||
|
logger.info("任务 {} 已被取消,停止收集结果", taskId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Boolean> result = future.get();
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
queried.putAll(result);
|
||||||
|
}
|
||||||
|
} catch (java.util.concurrent.CancellationException e) {
|
||||||
|
logger.info("线程任务已被取消: {}", taskId);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.info("线程任务被中断: {}", taskId);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取线程结果失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
taskExecutors.remove(taskId);
|
||||||
|
executor.shutdown();
|
||||||
|
try {
|
||||||
|
if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
|
||||||
|
logger.warn("线程池未能在60秒内正常关闭,强制关闭");
|
||||||
|
executor.shutdownNow();
|
||||||
|
if (!executor.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS)) {
|
||||||
|
logger.error("线程池强制关闭失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.warn("等待线程池关闭时被中断,强制关闭");
|
||||||
|
executor.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!queried.isEmpty()) {
|
||||||
|
cacheService.saveResults(queried);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 合并缓存和新查询结果
|
// 检查任务是否已被取消
|
||||||
|
if (cancelMap.getOrDefault(taskId, false)) {
|
||||||
|
logger.info("任务 {} 已被取消,停止处理结果", taskId);
|
||||||
|
synchronized (taskLock) {
|
||||||
|
if (taskId.equals(currentTaskId)) {
|
||||||
|
currentTaskId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progressMap.remove(taskId);
|
||||||
|
cancelMap.remove(taskId);
|
||||||
|
return JsonData.buildError("任务已取消");
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Boolean> allResults = new HashMap<>(cached);
|
Map<String, Boolean> allResults = new HashMap<>(cached);
|
||||||
allResults.putAll(queried);
|
allResults.putAll(queried);
|
||||||
|
|
||||||
// 6. 统计结果
|
|
||||||
List<Map<String, Object>> unregistered = new ArrayList<>();
|
List<Map<String, Object>> unregistered = new ArrayList<>();
|
||||||
int registeredCount = 0;
|
int registeredCount = 0;
|
||||||
|
|
||||||
for (Map.Entry<String, Boolean> entry : allResults.entrySet()) {
|
for (Map.Entry<String, Boolean> entry : allResults.entrySet()) {
|
||||||
if (!entry.getValue()) {
|
if (!entry.getValue()) {
|
||||||
Map<String, Object> m = new HashMap<>();
|
Map<String, Object> m = new HashMap<>();
|
||||||
@@ -120,46 +225,39 @@ public class TrademarkController {
|
|||||||
registeredCount++;
|
registeredCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long t = (System.currentTimeMillis() - start) / 1000;
|
long t = (System.currentTimeMillis() - start) / 1000;
|
||||||
int checkedCount = list.size();
|
|
||||||
int failedCount = 0;
|
|
||||||
|
|
||||||
Map<String, Object> res = new HashMap<>();
|
Map<String, Object> res = new HashMap<>();
|
||||||
res.put("total", list.size());
|
res.put("total", list.size());
|
||||||
res.put("checked", checkedCount);
|
res.put("checked", list.size());
|
||||||
res.put("registered", registeredCount);
|
res.put("registered", registeredCount);
|
||||||
res.put("unregistered", unregistered.size());
|
res.put("unregistered", unregistered.size());
|
||||||
res.put("failed", failedCount);
|
res.put("failed", 0);
|
||||||
res.put("data", unregistered);
|
res.put("data", unregistered);
|
||||||
res.put("duration", t + "秒");
|
res.put("duration", t + "秒");
|
||||||
|
|
||||||
logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒",
|
logger.info("完成: 共{}个,已注册{}个,未注册{}个,耗时{}秒",
|
||||||
list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t);
|
list.size(), registeredCount, unregistered.size(), t);
|
||||||
|
|
||||||
// 30秒后清理进度和取消标志
|
synchronized (taskLock) {
|
||||||
if (taskId != null) {
|
if (taskId.equals(currentTaskId)) {
|
||||||
String finalTaskId = taskId;
|
currentTaskId = null;
|
||||||
new Thread(() -> {
|
}
|
||||||
try { Thread.sleep(30000); } catch (InterruptedException ignored) {}
|
|
||||||
progressMap.remove(finalTaskId);
|
|
||||||
cancelMap.remove(finalTaskId);
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progressMap.remove(taskId);
|
||||||
|
cancelMap.remove(taskId);
|
||||||
|
|
||||||
return JsonData.buildSuccess(res);
|
return JsonData.buildSuccess(res);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("筛查失败", e);
|
logger.error("筛查失败", e);
|
||||||
return JsonData.buildError("筛查失败: " + e.getMessage());
|
return JsonData.buildError("筛查失败: " + e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
util.closeDriver();
|
|
||||||
cacheService.cleanExpired();
|
cacheService.cleanExpired();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询品牌筛查进度
|
|
||||||
*/
|
|
||||||
@GetMapping("/brandCheckProgress")
|
@GetMapping("/brandCheckProgress")
|
||||||
public JsonData getBrandCheckProgress(@RequestParam("taskId") String taskId) {
|
public JsonData getBrandCheckProgress(@RequestParam("taskId") String taskId) {
|
||||||
Integer current = progressMap.get(taskId);
|
Integer current = progressMap.get(taskId);
|
||||||
@@ -170,24 +268,59 @@ public class TrademarkController {
|
|||||||
result.put("current", current);
|
result.put("current", current);
|
||||||
return JsonData.buildSuccess(result);
|
return JsonData.buildSuccess(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消品牌筛查任务
|
|
||||||
*/
|
|
||||||
@PostMapping("/cancelBrandCheck")
|
@PostMapping("/cancelBrandCheck")
|
||||||
public JsonData cancelBrandCheck(@RequestBody Map<String, String> request) {
|
public JsonData cancelBrandCheck(@RequestBody Map<String, String> request) {
|
||||||
String taskId = request.get("taskId");
|
String taskId = request.get("taskId");
|
||||||
if (taskId != null) {
|
if (taskId != null) {
|
||||||
cancelMap.put(taskId, true);
|
forceTerminateTask(taskId);
|
||||||
logger.info("任务 {} 已标记为取消", taskId);
|
|
||||||
return JsonData.buildSuccess("任务已取消");
|
|
||||||
}
|
}
|
||||||
return JsonData.buildError("缺少taskId参数");
|
return JsonData.buildSuccess("任务已取消");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void forceTerminateTask(String taskId) {
|
||||||
|
logger.info("开始强制终止任务: {}", taskId);
|
||||||
|
|
||||||
|
cancelMap.put(taskId, true);
|
||||||
|
|
||||||
|
java.util.concurrent.ExecutorService executor = taskExecutors.remove(taskId);
|
||||||
|
if (executor != null && !executor.isShutdown()) {
|
||||||
|
logger.info("强制关闭任务 {} 的线程池", taskId);
|
||||||
|
executor.shutdownNow();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) {
|
||||||
|
logger.warn("任务 {} 的线程池未能在5秒内关闭", taskId);
|
||||||
|
} else {
|
||||||
|
logger.info("任务 {} 的线程池已成功关闭", taskId);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.warn("等待线程池关闭时被中断");
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SseEmitter emitter = sseEmitters.remove(taskId);
|
||||||
|
if (emitter != null) {
|
||||||
|
try {
|
||||||
|
emitter.send(SseEmitter.event().name("cancelled").data("任务已取消"));
|
||||||
|
emitter.complete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("关闭SSE连接失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressMap.remove(taskId);
|
||||||
|
|
||||||
|
synchronized (taskLock) {
|
||||||
|
if (taskId.equals(currentTaskId)) {
|
||||||
|
currentTaskId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("任务 {} 强制终止完成", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证Excel表头
|
|
||||||
*/
|
|
||||||
@PostMapping("/validateHeaders")
|
@PostMapping("/validateHeaders")
|
||||||
public JsonData validateHeaders(@RequestParam("file") MultipartFile file,
|
public JsonData validateHeaders(@RequestParam("file") MultipartFile file,
|
||||||
@RequestParam(value = "requiredHeaders", required = false) String requiredHeadersJson) {
|
@RequestParam(value = "requiredHeaders", required = false) String requiredHeadersJson) {
|
||||||
@@ -195,41 +328,40 @@ public class TrademarkController {
|
|||||||
Map<String, Object> fullData = ExcelParseUtil.parseFullExcel(file);
|
Map<String, Object> fullData = ExcelParseUtil.parseFullExcel(file);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<String> headers = (List<String>) fullData.get("headers");
|
List<String> headers = (List<String>) fullData.get("headers");
|
||||||
|
|
||||||
if (headers == null || headers.isEmpty()) {
|
if (headers == null || headers.isEmpty()) {
|
||||||
return JsonData.buildError("无法读取Excel表头");
|
return JsonData.buildError("无法读取Excel表头");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("headers", headers);
|
result.put("headers", headers);
|
||||||
|
|
||||||
// 如果提供了必需表头,进行验证
|
|
||||||
if (requiredHeadersJson != null && !requiredHeadersJson.trim().isEmpty()) {
|
if (requiredHeadersJson != null && !requiredHeadersJson.trim().isEmpty()) {
|
||||||
List<String> requiredHeaders = objectMapper.readValue(requiredHeadersJson,
|
List<String> requiredHeaders = objectMapper.readValue(requiredHeadersJson,
|
||||||
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||||
|
|
||||||
List<String> missing = new ArrayList<>();
|
List<String> missing = new ArrayList<>();
|
||||||
for (String required : requiredHeaders) {
|
for (String required : requiredHeaders) {
|
||||||
if (!headers.contains(required)) {
|
if (!headers.contains(required)) {
|
||||||
missing.add(required);
|
missing.add(required);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.put("valid", missing.isEmpty());
|
result.put("valid", missing.isEmpty());
|
||||||
result.put("missing", missing);
|
result.put("missing", missing);
|
||||||
|
|
||||||
if (!missing.isEmpty()) {
|
if (!missing.isEmpty()) {
|
||||||
return JsonData.buildError("缺少必需的列: " + String.join(", ", missing));
|
return JsonData.buildError("缺少必需的列: " + String.join(", ", missing));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonData.buildSuccess(result);
|
return JsonData.buildSuccess(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("验证表头失败", e);
|
logger.error("验证表头失败", e);
|
||||||
return JsonData.buildError("验证失败: " + e.getMessage());
|
return JsonData.buildError("验证失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从Excel提取品牌列表(同时返回完整Excel数据)
|
* 从Excel提取品牌列表(同时返回完整Excel数据)
|
||||||
*/
|
*/
|
||||||
@@ -238,10 +370,10 @@ public class TrademarkController {
|
|||||||
try {
|
try {
|
||||||
List<String> brands = ExcelParseUtil.parseColumnByName(file, "品牌");
|
List<String> brands = ExcelParseUtil.parseColumnByName(file, "品牌");
|
||||||
if (brands.isEmpty()) return JsonData.buildError("未找到品牌列或品牌数据为空");
|
if (brands.isEmpty()) return JsonData.buildError("未找到品牌列或品牌数据为空");
|
||||||
|
|
||||||
// 读取完整Excel数据
|
// 读取完整Excel数据
|
||||||
Map<String, Object> fullData = ExcelParseUtil.parseFullExcel(file);
|
Map<String, Object> fullData = ExcelParseUtil.parseFullExcel(file);
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("total", brands.size());
|
result.put("total", brands.size());
|
||||||
result.put("brands", brands);
|
result.put("brands", brands);
|
||||||
@@ -262,35 +394,35 @@ public class TrademarkController {
|
|||||||
if (asinsJson == null || asinsJson.trim().isEmpty()) {
|
if (asinsJson == null || asinsJson.trim().isEmpty()) {
|
||||||
return JsonData.buildError("ASIN列表不能为空");
|
return JsonData.buildError("ASIN列表不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用Jackson解析JSON数组
|
// 使用Jackson解析JSON数组
|
||||||
List<String> asins;
|
List<String> asins;
|
||||||
try {
|
try {
|
||||||
asins = objectMapper.readValue(asinsJson,
|
asins = objectMapper.readValue(asinsJson,
|
||||||
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("解析ASIN列表JSON失败: {}", asinsJson, e);
|
logger.error("解析ASIN列表JSON失败: {}", asinsJson, e);
|
||||||
return JsonData.buildError("ASIN列表格式错误: " + e.getMessage());
|
return JsonData.buildError("ASIN列表格式错误: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asins == null || asins.isEmpty()) {
|
if (asins == null || asins.isEmpty()) {
|
||||||
return JsonData.buildError("ASIN列表不能为空");
|
return JsonData.buildError("ASIN列表不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("接收到ASIN过滤请求,ASIN数量: {}", asins.size());
|
logger.info("接收到ASIN过滤请求,ASIN数量: {}", asins.size());
|
||||||
|
|
||||||
Map<String, Object> result = ExcelParseUtil.filterExcelByAsins(file, asins);
|
Map<String, Object> result = ExcelParseUtil.filterExcelByAsins(file, asins);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<Map<String, Object>> filteredRows = (List<Map<String, Object>>) result.get("filteredRows");
|
List<Map<String, Object>> filteredRows = (List<Map<String, Object>>) result.get("filteredRows");
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("headers", result.get("headers"));
|
response.put("headers", result.get("headers"));
|
||||||
response.put("filteredRows", filteredRows);
|
response.put("filteredRows", filteredRows);
|
||||||
response.put("total", filteredRows.size());
|
response.put("total", filteredRows.size());
|
||||||
|
|
||||||
logger.info("ASIN过滤完成,过滤出 {} 行数据", filteredRows.size());
|
logger.info("ASIN过滤完成,过滤出 {} 行数据", filteredRows.size());
|
||||||
|
|
||||||
return JsonData.buildSuccess(response);
|
return JsonData.buildSuccess(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("根据ASIN过滤失败", e);
|
logger.error("根据ASIN过滤失败", e);
|
||||||
@@ -307,35 +439,35 @@ public class TrademarkController {
|
|||||||
if (brandsJson == null || brandsJson.trim().isEmpty()) {
|
if (brandsJson == null || brandsJson.trim().isEmpty()) {
|
||||||
return JsonData.buildError("品牌列表不能为空");
|
return JsonData.buildError("品牌列表不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用Jackson解析JSON数组
|
// 使用Jackson解析JSON数组
|
||||||
List<String> brands;
|
List<String> brands;
|
||||||
try {
|
try {
|
||||||
brands = objectMapper.readValue(brandsJson,
|
brands = objectMapper.readValue(brandsJson,
|
||||||
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("解析品牌列表JSON失败: {}", brandsJson, e);
|
logger.error("解析品牌列表JSON失败: {}", brandsJson, e);
|
||||||
return JsonData.buildError("品牌列表格式错误: " + e.getMessage());
|
return JsonData.buildError("品牌列表格式错误: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (brands == null || brands.isEmpty()) {
|
if (brands == null || brands.isEmpty()) {
|
||||||
return JsonData.buildError("品牌列表不能为空");
|
return JsonData.buildError("品牌列表不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("接收到品牌过滤请求,品牌数量: {}", brands.size());
|
logger.info("接收到品牌过滤请求,品牌数量: {}", brands.size());
|
||||||
|
|
||||||
Map<String, Object> result = ExcelParseUtil.filterExcelByBrands(file, brands);
|
Map<String, Object> result = ExcelParseUtil.filterExcelByBrands(file, brands);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<Map<String, Object>> filteredRows = (List<Map<String, Object>>) result.get("filteredRows");
|
List<Map<String, Object>> filteredRows = (List<Map<String, Object>>) result.get("filteredRows");
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("headers", result.get("headers"));
|
response.put("headers", result.get("headers"));
|
||||||
response.put("filteredRows", filteredRows);
|
response.put("filteredRows", filteredRows);
|
||||||
response.put("total", filteredRows.size());
|
response.put("total", filteredRows.size());
|
||||||
|
|
||||||
logger.info("品牌过滤完成,过滤出 {} 行数据", filteredRows.size());
|
logger.info("品牌过滤完成,过滤出 {} 行数据", filteredRows.size());
|
||||||
|
|
||||||
return JsonData.buildSuccess(response);
|
return JsonData.buildSuccess(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("根据品牌过滤失败", e);
|
logger.error("根据品牌过滤失败", e);
|
||||||
@@ -347,13 +479,13 @@ public class TrademarkController {
|
|||||||
* 保存商标查询会话
|
* 保存商标查询会话
|
||||||
*/
|
*/
|
||||||
@PostMapping("/saveSession")
|
@PostMapping("/saveSession")
|
||||||
public JsonData saveSession(@RequestBody Map<String, Object> sessionData,
|
public JsonData saveSession(@RequestBody Map<String, Object> sessionData,
|
||||||
@RequestHeader(value = "username", required = false) String username) {
|
@RequestHeader(value = "username", required = false) String username) {
|
||||||
try {
|
try {
|
||||||
if (username == null || username.trim().isEmpty()) {
|
if (username == null || username.trim().isEmpty()) {
|
||||||
username = "default";
|
username = "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
String sessionId = UUID.randomUUID().toString();
|
String sessionId = UUID.randomUUID().toString();
|
||||||
TrademarkSessionEntity entity = new TrademarkSessionEntity();
|
TrademarkSessionEntity entity = new TrademarkSessionEntity();
|
||||||
entity.setSessionId(sessionId);
|
entity.setSessionId(sessionId);
|
||||||
@@ -364,14 +496,14 @@ public class TrademarkController {
|
|||||||
entity.setHeaders(objectMapper.writeValueAsString(sessionData.get("headers")));
|
entity.setHeaders(objectMapper.writeValueAsString(sessionData.get("headers")));
|
||||||
entity.setTaskProgress(objectMapper.writeValueAsString(sessionData.get("taskProgress")));
|
entity.setTaskProgress(objectMapper.writeValueAsString(sessionData.get("taskProgress")));
|
||||||
entity.setQueryStatus((String) sessionData.get("queryStatus"));
|
entity.setQueryStatus((String) sessionData.get("queryStatus"));
|
||||||
|
|
||||||
sessionRepository.save(entity);
|
sessionRepository.save(entity);
|
||||||
|
|
||||||
// 清理7天前的数据
|
// 清理7天前的数据
|
||||||
sessionRepository.deleteByCreatedAtBefore(LocalDateTime.now().minusDays(7));
|
sessionRepository.deleteByCreatedAtBefore(LocalDateTime.now().minusDays(7));
|
||||||
|
|
||||||
logger.info("保存商标查询会话: {} (用户: {})", sessionId, username);
|
logger.info("保存商标查询会话: {} (用户: {})", sessionId, username);
|
||||||
|
|
||||||
Map<String, String> result = new HashMap<>();
|
Map<String, String> result = new HashMap<>();
|
||||||
result.put("sessionId", sessionId);
|
result.put("sessionId", sessionId);
|
||||||
return JsonData.buildSuccess(result);
|
return JsonData.buildSuccess(result);
|
||||||
@@ -386,17 +518,17 @@ public class TrademarkController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/getSession")
|
@GetMapping("/getSession")
|
||||||
public JsonData getSession(@RequestParam("sessionId") String sessionId,
|
public JsonData getSession(@RequestParam("sessionId") String sessionId,
|
||||||
@RequestHeader(value = "username", required = false) String username) {
|
@RequestHeader(value = "username", required = false) String username) {
|
||||||
try {
|
try {
|
||||||
if (username == null || username.trim().isEmpty()) {
|
if (username == null || username.trim().isEmpty()) {
|
||||||
username = "default";
|
username = "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<TrademarkSessionEntity> opt = sessionRepository.findBySessionIdAndUsername(sessionId, username);
|
Optional<TrademarkSessionEntity> opt = sessionRepository.findBySessionIdAndUsername(sessionId, username);
|
||||||
if (!opt.isPresent()) {
|
if (!opt.isPresent()) {
|
||||||
return JsonData.buildError("会话不存在或已过期");
|
return JsonData.buildError("会话不存在或已过期");
|
||||||
}
|
}
|
||||||
|
|
||||||
TrademarkSessionEntity entity = opt.get();
|
TrademarkSessionEntity entity = opt.get();
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("fileName", entity.getFileName());
|
result.put("fileName", entity.getFileName());
|
||||||
@@ -405,7 +537,7 @@ public class TrademarkController {
|
|||||||
result.put("headers", objectMapper.readValue(entity.getHeaders(), List.class));
|
result.put("headers", objectMapper.readValue(entity.getHeaders(), List.class));
|
||||||
result.put("taskProgress", objectMapper.readValue(entity.getTaskProgress(), Map.class));
|
result.put("taskProgress", objectMapper.readValue(entity.getTaskProgress(), Map.class));
|
||||||
result.put("queryStatus", entity.getQueryStatus());
|
result.put("queryStatus", entity.getQueryStatus());
|
||||||
|
|
||||||
logger.info("恢复商标查询会话: {} (用户: {})", sessionId, username);
|
logger.info("恢复商标查询会话: {} (用户: {})", sessionId, username);
|
||||||
return JsonData.buildSuccess(result);
|
return JsonData.buildSuccess(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -415,7 +547,7 @@ public class TrademarkController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 方舟精选任务管理接口 ====================
|
// ==================== 方舟精选任务管理接口 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取方舟精选任务列表
|
* 获取方舟精选任务列表
|
||||||
* 从第三方 API 下载 Excel 并解析过滤数据
|
* 从第三方 API 下载 Excel 并解析过滤数据
|
||||||
@@ -427,33 +559,33 @@ public class TrademarkController {
|
|||||||
String token = fangzhouApi.getToken();
|
String token = fangzhouApi.getToken();
|
||||||
JsonNode dNode = fangzhouApi.pollTask(token, 6, 5000);
|
JsonNode dNode = fangzhouApi.pollTask(token, 6, 5000);
|
||||||
String downloadUrl = dNode.get("download_url").asText();
|
String downloadUrl = dNode.get("download_url").asText();
|
||||||
|
|
||||||
if (downloadUrl == null || downloadUrl.isEmpty()) {
|
if (downloadUrl == null || downloadUrl.isEmpty()) {
|
||||||
return JsonData.buildError("下载链接生成超时");
|
return JsonData.buildError("下载链接生成超时");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 下载并解析 Excel
|
// 2. 下载并解析 Excel
|
||||||
String tempFilePath = System.getProperty("java.io.tmpdir") + "/trademark_" + System.currentTimeMillis() + ".xlsx";
|
String tempFilePath = System.getProperty("java.io.tmpdir") + "/trademark_" + System.currentTimeMillis() + ".xlsx";
|
||||||
HttpUtil.downloadFile(downloadUrl, FileUtil.file(tempFilePath));
|
HttpUtil.downloadFile(downloadUrl, FileUtil.file(tempFilePath));
|
||||||
|
|
||||||
List<Map<String, Object>> filteredData = new ArrayList<>();
|
List<Map<String, Object>> filteredData = new ArrayList<>();
|
||||||
List<String> excelHeaders = new ArrayList<>();
|
List<String> excelHeaders = new ArrayList<>();
|
||||||
ExcelReader reader = null;
|
ExcelReader reader = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
reader = ExcelUtil.getReader(FileUtil.file(tempFilePath));
|
reader = ExcelUtil.getReader(FileUtil.file(tempFilePath));
|
||||||
List<List<Object>> rows = reader.read();
|
List<List<Object>> rows = reader.read();
|
||||||
|
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
throw new RuntimeException("Excel文件为空");
|
throw new RuntimeException("Excel文件为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取表头
|
// 读取表头
|
||||||
List<Object> headerRow = rows.get(0);
|
List<Object> headerRow = rows.get(0);
|
||||||
for (Object cell : headerRow) {
|
for (Object cell : headerRow) {
|
||||||
excelHeaders.add(cell != null ? cell.toString().trim() : "");
|
excelHeaders.add(cell != null ? cell.toString().trim() : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到商标类型列的索引
|
// 找到商标类型列的索引
|
||||||
int trademarkTypeIndex = -1;
|
int trademarkTypeIndex = -1;
|
||||||
for (int i = 0; i < excelHeaders.size(); i++) {
|
for (int i = 0; i < excelHeaders.size(); i++) {
|
||||||
@@ -462,11 +594,11 @@ public class TrademarkController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trademarkTypeIndex < 0) {
|
if (trademarkTypeIndex < 0) {
|
||||||
throw new RuntimeException("未找到'商标类型'列");
|
throw new RuntimeException("未找到'商标类型'列");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤TM和未注册数据
|
// 过滤TM和未注册数据
|
||||||
for (int i = 1; i < rows.size(); i++) {
|
for (int i = 1; i < rows.size(); i++) {
|
||||||
List<Object> row = rows.get(i);
|
List<Object> row = rows.get(i);
|
||||||
@@ -487,16 +619,16 @@ public class TrademarkController {
|
|||||||
}
|
}
|
||||||
FileUtil.del(tempFilePath);
|
FileUtil.del(tempFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 返回结果
|
// 6. 返回结果
|
||||||
Map<String, Object> combinedResult = new HashMap<>();
|
Map<String, Object> combinedResult = new HashMap<>();
|
||||||
combinedResult.put("original", dNode);
|
combinedResult.put("original", dNode);
|
||||||
combinedResult.put("filtered", filteredData);
|
combinedResult.put("filtered", filteredData);
|
||||||
combinedResult.put("headers", excelHeaders);
|
combinedResult.put("headers", excelHeaders);
|
||||||
|
|
||||||
logger.info("任务获取成功,过滤出 {} 条数据", filteredData.size());
|
logger.info("任务获取成功,过滤出 {} 条数据", filteredData.size());
|
||||||
return JsonData.buildSuccess(combinedResult);
|
return JsonData.buildSuccess(combinedResult);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("获取任务失败", e);
|
logger.error("获取任务失败", e);
|
||||||
return JsonData.buildError("获取任务失败: " + e.getMessage());
|
return JsonData.buildError("获取任务失败: " + e.getMessage());
|
||||||
@@ -509,21 +641,35 @@ public class TrademarkController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/newTask")
|
@PostMapping("/newTask")
|
||||||
public JsonData newTask(@RequestParam("file") MultipartFile file) {
|
public JsonData newTask(@RequestParam("file") MultipartFile file) {
|
||||||
|
// 防止重复上传:如果已有上传任务在进行,直接拒绝
|
||||||
|
synchronized (uploadLock) {
|
||||||
|
if (isUploadingFile) {
|
||||||
|
logger.warn("文件上传被拒绝:已有上传任务正在进行中");
|
||||||
|
return JsonData.buildError("请勿重复点击,上传任务进行中");
|
||||||
|
}
|
||||||
|
isUploadingFile = true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 获取 Token 并上传文件
|
// 1. 获取 Token 并上传文件
|
||||||
String token = fangzhouApi.getToken();
|
String token = fangzhouApi.getToken();
|
||||||
JsonNode jsonNode = fangzhouApi.uploadFile(file, token);
|
JsonNode jsonNode = fangzhouApi.uploadFile(file, token);
|
||||||
|
|
||||||
// 2. 返回结果
|
// 2. 返回结果
|
||||||
if (jsonNode.get("S").asInt() == 1) {
|
if (jsonNode.get("S").asInt() == 1) {
|
||||||
logger.info("任务创建成功: {}", file.getOriginalFilename());
|
logger.info("任务创建成功: {}", file.getOriginalFilename());
|
||||||
return JsonData.buildSuccess(jsonNode.toString());
|
return JsonData.buildSuccess(jsonNode.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonData.buildError(jsonNode.get("M").asText());
|
return JsonData.buildError(jsonNode.get("M").asText());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("创建任务失败", e);
|
logger.error("创建任务失败", e);
|
||||||
return JsonData.buildError("创建任务失败: " + e.getMessage());
|
return JsonData.buildError("创建任务失败: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
// 释放上传锁
|
||||||
|
synchronized (uploadLock) {
|
||||||
|
isUploadingFile = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,6 +39,10 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service {
|
|||||||
private final RestTemplate noSslRestTemplate = createNoSslRestTemplate();
|
private final RestTemplate noSslRestTemplate = createNoSslRestTemplate();
|
||||||
@Autowired
|
@Autowired
|
||||||
private ErrorReporter errorReporter;
|
private ErrorReporter errorReporter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建忽略SSL证书的RestTemplate
|
||||||
|
*/
|
||||||
private RestTemplate createNoSslRestTemplate() {
|
private RestTemplate createNoSslRestTemplate() {
|
||||||
try {
|
try {
|
||||||
TrustManager[] trustManagers = new TrustManager[] {
|
TrustManager[] trustManagers = new TrustManager[] {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package com.tashow.erp.service.impl;
|
package com.tashow.erp.service.impl;
|
||||||
|
|
||||||
|
import com.tashow.erp.common.AmazonConstants;
|
||||||
|
import com.tashow.erp.common.CacheConstants;
|
||||||
import com.tashow.erp.entity.AmazonProductEntity;
|
import com.tashow.erp.entity.AmazonProductEntity;
|
||||||
import com.tashow.erp.repository.AmazonProductRepository;
|
import com.tashow.erp.repository.AmazonProductRepository;
|
||||||
import com.tashow.erp.service.AmazonScrapingService;
|
import com.tashow.erp.service.AmazonScrapingService;
|
||||||
import com.tashow.erp.utils.DataReportUtil;
|
import com.tashow.erp.utils.DataReportUtil;
|
||||||
import com.tashow.erp.utils.ErrorReporter;
|
import com.tashow.erp.utils.ErrorReporter;
|
||||||
import com.tashow.erp.utils.RakutenProxyUtil;
|
import com.tashow.erp.utils.RakutenProxyUtil;
|
||||||
|
import com.tashow.erp.utils.UrlBuilder;
|
||||||
|
import com.tashow.erp.utils.ValidationUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import us.codecraft.webmagic.Page;
|
import us.codecraft.webmagic.Page;
|
||||||
@@ -12,13 +17,13 @@ import us.codecraft.webmagic.Site;
|
|||||||
import us.codecraft.webmagic.Spider;
|
import us.codecraft.webmagic.Spider;
|
||||||
import us.codecraft.webmagic.processor.PageProcessor;
|
import us.codecraft.webmagic.processor.PageProcessor;
|
||||||
import us.codecraft.webmagic.selector.Html;
|
import us.codecraft.webmagic.selector.Html;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
/**
|
/**
|
||||||
* 亚马逊数据采集服务实现类
|
* 亚马逊数据采集服务实现
|
||||||
*
|
* 负责批量采集亚马逊商品信息并缓存
|
||||||
* @author ruoyi
|
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class AmazonScrapingServiceImpl implements AmazonScrapingService, PageProcessor {
|
public class AmazonScrapingServiceImpl implements AmazonScrapingService, PageProcessor {
|
||||||
@@ -42,7 +47,7 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro
|
|||||||
String url = page.getUrl().toString();
|
String url = page.getUrl().toString();
|
||||||
// 提取ASIN
|
// 提取ASIN
|
||||||
String asin = html.xpath("//input[@id='ASIN']/@value").toString();
|
String asin = html.xpath("//input[@id='ASIN']/@value").toString();
|
||||||
if (isEmpty(asin)) {
|
if (ValidationUtils.isEmpty(asin)) {
|
||||||
String[] parts = url.split("/dp/");
|
String[] parts = url.split("/dp/");
|
||||||
if (parts.length > 1) asin = parts[1].split("/")[0].split("\\?")[0];
|
if (parts.length > 1) asin = parts[1].split("/")[0].split("\\?")[0];
|
||||||
}
|
}
|
||||||
@@ -50,33 +55,33 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro
|
|||||||
String priceSymbol = html.xpath("//span[@class='a-price-symbol']/text()").toString();
|
String priceSymbol = html.xpath("//span[@class='a-price-symbol']/text()").toString();
|
||||||
String priceWhole = html.xpath("//span[@class='a-price-whole']/text()").toString();
|
String priceWhole = html.xpath("//span[@class='a-price-whole']/text()").toString();
|
||||||
String price = null;
|
String price = null;
|
||||||
if (!isEmpty(priceSymbol) && !isEmpty(priceWhole)) {
|
if (ValidationUtils.isNotEmpty(priceSymbol) && ValidationUtils.isNotEmpty(priceWhole)) {
|
||||||
price = priceSymbol + priceWhole;
|
price = priceSymbol + priceWhole;
|
||||||
}
|
}
|
||||||
if (isEmpty(price)) {
|
if (ValidationUtils.isEmpty(price)) {
|
||||||
price = html.xpath("//span[@class='a-price-range']/text()").toString();
|
price = html.xpath("//span[@class='a-price-range']/text()").toString();
|
||||||
}
|
}
|
||||||
// 提取卖家
|
// 提取卖家
|
||||||
String seller = html.xpath("//a[@id='sellerProfileTriggerId']/text()").toString();
|
String seller = html.xpath("//a[@id='sellerProfileTriggerId']/text()").toString();
|
||||||
if (isEmpty(seller)) {
|
if (ValidationUtils.isEmpty(seller)) {
|
||||||
seller = html.xpath("//span[@class='a-size-small offer-display-feature-text-message']/text()").toString();
|
seller = html.xpath("//span[@class='a-size-small offer-display-feature-text-message']/text()").toString();
|
||||||
}
|
}
|
||||||
// 关键数据为空时重试
|
// 关键数据为空时重试
|
||||||
if (isEmpty(price) && isEmpty(seller)) {
|
if (ValidationUtils.isEmpty(price) && ValidationUtils.isEmpty(seller)) {
|
||||||
throw new RuntimeException("Retry this page");
|
throw new RuntimeException("Retry this page");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmpty(price)) errorReporter.reportDataEmpty("amazon", asin, price);
|
if (ValidationUtils.isEmpty(price)) errorReporter.reportDataEmpty(AmazonConstants.DATA_TYPE.toLowerCase(), asin, price);
|
||||||
if (isEmpty(seller)) errorReporter.reportDataEmpty("amazon", asin, seller);
|
if (ValidationUtils.isEmpty(seller)) errorReporter.reportDataEmpty(AmazonConstants.DATA_TYPE.toLowerCase(), asin, seller);
|
||||||
|
|
||||||
AmazonProductEntity entity = new AmazonProductEntity();
|
AmazonProductEntity entity = new AmazonProductEntity();
|
||||||
entity.setAsin(asin == null ? "" : asin);
|
entity.setAsin(asin == null ? "" : asin);
|
||||||
|
|
||||||
entity.setPrice(price);
|
entity.setPrice(price);
|
||||||
entity.setSeller(seller);
|
entity.setSeller(seller);
|
||||||
resultCache.put(asin, entity);
|
resultCache.put(asin, entity);
|
||||||
page.putField("entity", entity);
|
page.putField("entity", entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取WebMagic站点配置
|
* 获取WebMagic站点配置
|
||||||
*/
|
*/
|
||||||
@@ -86,31 +91,30 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量获取产品信息
|
* 批量获取亚马逊商品信息
|
||||||
|
* 优先从缓存读取,缓存未命中时实时采集
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<AmazonProductEntity> batchGetProductInfo(List<String> asinList, String batchId, String region) {
|
public List<AmazonProductEntity> batchGetProductInfo(List<String> asinList, String batchId, String region) {
|
||||||
String sessionId = (batchId != null) ? batchId : "SINGLE_" + UUID.randomUUID();
|
String sessionId = (batchId != null) ? batchId : AmazonConstants.SESSION_PREFIX + UUID.randomUUID();
|
||||||
LocalDateTime batchTime = LocalDateTime.now();
|
|
||||||
|
|
||||||
resultCache.clear();
|
resultCache.clear();
|
||||||
|
|
||||||
// 第一步:清理1小时前的所有旧数据
|
// 清理过期缓存数据
|
||||||
amazonProductRepository.deleteAllDataBefore(LocalDateTime.now().minusHours(1));
|
amazonProductRepository.deleteAllDataBefore(LocalDateTime.now().minusHours(CacheConstants.DATA_RETENTION_HOURS));
|
||||||
|
|
||||||
// 优化:批次内复用代理检测和工具实例
|
// 批次内复用代理检测和下载器实例
|
||||||
RakutenProxyUtil proxyUtil = new RakutenProxyUtil();
|
RakutenProxyUtil proxyUtil = new RakutenProxyUtil();
|
||||||
String sampleUrl = buildAmazonUrl(region, "B00000000");
|
String sampleUrl = UrlBuilder.buildAmazonUrl(region, "B00000000");
|
||||||
var proxyDownloader = proxyUtil.createProxyDownloader(proxyUtil.detectSystemProxy(sampleUrl));
|
var proxyDownloader = proxyUtil.createProxyDownloader(proxyUtil.detectSystemProxy(sampleUrl));
|
||||||
|
|
||||||
// 第二步:处理每个ASIN
|
// 处理每个ASIN
|
||||||
Map<String, AmazonProductEntity> allProducts = new HashMap<>();
|
Map<String, AmazonProductEntity> allProducts = new HashMap<>();
|
||||||
for (String asin : asinList.stream().distinct().toList()) {
|
for (String asin : asinList.stream().distinct().toList()) {
|
||||||
if (asin == null || asin.trim().isEmpty()) continue;
|
if (asin == null || asin.trim().isEmpty()) continue;
|
||||||
String cleanAsin = asin.replaceAll("[^a-zA-Z0-9]", "");
|
String cleanAsin = asin.replaceAll("[^a-zA-Z0-9]", "");
|
||||||
|
|
||||||
Optional<AmazonProductEntity> cached = amazonProductRepository.findByAsinAndRegion(cleanAsin, region);
|
Optional<AmazonProductEntity> cached = amazonProductRepository.findByAsinAndRegion(cleanAsin, region);
|
||||||
if (cached.isPresent() && !isEmpty(cached.get().getPrice()) && !isEmpty(cached.get().getSeller())) {
|
if (cached.isPresent() && ValidationUtils.isNotEmpty(cached.get().getPrice()) && ValidationUtils.isNotEmpty(cached.get().getSeller())) {
|
||||||
AmazonProductEntity cachedEntity = cached.get();
|
AmazonProductEntity cachedEntity = cached.get();
|
||||||
AmazonProductEntity entity = new AmazonProductEntity();
|
AmazonProductEntity entity = new AmazonProductEntity();
|
||||||
entity.setAsin(cachedEntity.getAsin());
|
entity.setAsin(cachedEntity.getAsin());
|
||||||
@@ -122,7 +126,7 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro
|
|||||||
amazonProductRepository.save(entity);
|
amazonProductRepository.save(entity);
|
||||||
allProducts.put(cleanAsin, entity);
|
allProducts.put(cleanAsin, entity);
|
||||||
} else {
|
} else {
|
||||||
String url = buildAmazonUrl(region, cleanAsin);
|
String url = UrlBuilder.buildAmazonUrl(region, cleanAsin);
|
||||||
this.site = configureSiteForRegion(region);
|
this.site = configureSiteForRegion(region);
|
||||||
synchronized (spiderLock) {
|
synchronized (spiderLock) {
|
||||||
activeSpider = Spider.create(this).addUrl(url).setDownloader(proxyDownloader).thread(1);
|
activeSpider = Spider.create(this).addUrl(url).setDownloader(proxyDownloader).thread(1);
|
||||||
@@ -135,51 +139,38 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro
|
|||||||
entity.setSessionId(sessionId);
|
entity.setSessionId(sessionId);
|
||||||
entity.setUpdatedAt(LocalDateTime.now());
|
entity.setUpdatedAt(LocalDateTime.now());
|
||||||
amazonProductRepository.save(entity);
|
amazonProductRepository.save(entity);
|
||||||
dataReportUtil.reportDataCollection("AMAZON", 1, "0");
|
dataReportUtil.reportDataCollection(AmazonConstants.DATA_TYPE, 1, "0");
|
||||||
allProducts.put(cleanAsin, entity);
|
allProducts.put(cleanAsin, entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ArrayList<>(allProducts.values());
|
return new ArrayList<>(allProducts.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEmpty(String str) {
|
|
||||||
return str == null || str.trim().isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据地区构建Amazon URL
|
* 根据地区配置WebMagic站点
|
||||||
*/
|
|
||||||
private String buildAmazonUrl(String region, String asin) {
|
|
||||||
if ("US".equals(region)) {
|
|
||||||
return "https://www.amazon.com/dp/" + asin;
|
|
||||||
}
|
|
||||||
return "https://www.amazon.co.jp/dp/" + asin;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 根据地区配置Site
|
|
||||||
*/
|
*/
|
||||||
private Site configureSiteForRegion(String region) {
|
private Site configureSiteForRegion(String region) {
|
||||||
boolean isUS = "US".equals(region);
|
boolean isUS = AmazonConstants.REGION_US.equals(region);
|
||||||
return Site.me()
|
return Site.me()
|
||||||
.setRetryTimes(3)
|
.setRetryTimes(AmazonConstants.RETRY_TIMES)
|
||||||
.setSleepTime(2000 + random.nextInt(2000))
|
.setSleepTime(AmazonConstants.SLEEP_TIME_BASE + random.nextInt(AmazonConstants.SLEEP_TIME_RANDOM))
|
||||||
.setTimeOut(20000)
|
.setTimeOut(AmazonConstants.TIMEOUT_MS)
|
||||||
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
|
.setUserAgent(AmazonConstants.USER_AGENT)
|
||||||
.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
|
.addHeader("accept", AmazonConstants.HEADER_ACCEPT)
|
||||||
.addHeader("accept-encoding", "gzip, deflate, br")
|
.addHeader("accept-encoding", AmazonConstants.HEADER_ACCEPT_ENCODING)
|
||||||
.addHeader("accept-language", isUS ? "zh-CN,zh;q=0.9,en;q=0.8" : "ja,en;q=0.9,zh-CN;q=0.8")
|
.addHeader("accept-language", isUS ? AmazonConstants.HEADER_ACCEPT_LANGUAGE_US : AmazonConstants.HEADER_ACCEPT_LANGUAGE_JP)
|
||||||
.addHeader("cache-control", "max-age=0")
|
.addHeader("cache-control", AmazonConstants.HEADER_CACHE_CONTROL)
|
||||||
.addHeader("referer", isUS ? "https://www.amazon.com/" : "https://www.amazon.co.jp/")
|
.addHeader("referer", isUS ? AmazonConstants.DOMAIN_US + "/" : AmazonConstants.DOMAIN_JP + "/")
|
||||||
.addHeader("sec-fetch-site", "none")
|
.addHeader("sec-fetch-site", "none")
|
||||||
.addHeader("sec-fetch-mode", "navigate")
|
.addHeader("sec-fetch-mode", "navigate")
|
||||||
.addHeader("sec-fetch-user", "?1")
|
.addHeader("sec-fetch-user", "?1")
|
||||||
.addHeader("sec-fetch-dest", "document")
|
.addHeader("sec-fetch-dest", "document")
|
||||||
.addCookie("i18n-prefs", isUS ? "USD" : "JPY")
|
.addCookie("i18n-prefs", isUS ? AmazonConstants.COOKIE_I18N_PREFS_US : AmazonConstants.COOKIE_I18N_PREFS_JP)
|
||||||
.addCookie(isUS ? "lc-main" : "lc-acbjp", isUS ? "en_US" : "zh_CN")
|
.addCookie(isUS ? "lc-main" : "lc-acbjp", isUS ? AmazonConstants.COOKIE_LC_US : AmazonConstants.COOKIE_LC_JP)
|
||||||
.addCookie("session-id", isUS ? "134-6097934-2082600" : "358-1261309-0483141")
|
.addCookie("session-id", isUS ? AmazonConstants.COOKIE_SESSION_ID_US : AmazonConstants.COOKIE_SESSION_ID_JP)
|
||||||
.addCookie("session-id-time", "2082787201l")
|
.addCookie("session-id-time", AmazonConstants.COOKIE_SESSION_ID_TIME)
|
||||||
.addCookie(isUS ? "ubid-main" : "ubid-acbjp", isUS ? "132-7547587-3056927" : "357-8224002-9668932")
|
.addCookie(isUS ? "ubid-main" : "ubid-acbjp", isUS ? AmazonConstants.COOKIE_UBID_US : AmazonConstants.COOKIE_UBID_JP)
|
||||||
.addCookie("skin", "noskin");
|
.addCookie("skin", AmazonConstants.COOKIE_SKIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,10 @@ import org.springframework.stereotype.Service;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证服务实现
|
||||||
|
* 负责客户端信息管理和错误上报
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class AuthServiceImpl {
|
public class AuthServiceImpl {
|
||||||
|
|
||||||
@@ -22,6 +26,9 @@ public class AuthServiceImpl {
|
|||||||
@Getter
|
@Getter
|
||||||
private String clientId = DeviceUtils.generateDeviceId();
|
private String clientId = DeviceUtils.generateDeviceId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端信息
|
||||||
|
*/
|
||||||
public Map<String, Object> getClientInfo() {
|
public Map<String, Object> getClientInfo() {
|
||||||
Map<String, Object> info = new HashMap<>();
|
Map<String, Object> info = new HashMap<>();
|
||||||
info.put("clientId", clientId);
|
info.put("clientId", clientId);
|
||||||
@@ -30,6 +37,9 @@ public class AuthServiceImpl {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上报错误信息
|
||||||
|
*/
|
||||||
public void reportError(String errorType, String errorMessage, Exception e) {
|
public void reportError(String errorType, String errorMessage, Exception e) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> errorData = new HashMap<>();
|
Map<String, Object> errorData = new HashMap<>();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.tashow.erp.service.impl;
|
package com.tashow.erp.service.impl;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.tashow.erp.common.BanmaConstants;
|
||||||
import com.tashow.erp.entity.BanmaOrderEntity;
|
import com.tashow.erp.entity.BanmaOrderEntity;
|
||||||
import com.tashow.erp.repository.BanmaOrderRepository;
|
import com.tashow.erp.repository.BanmaOrderRepository;
|
||||||
import com.tashow.erp.service.BanmaOrderService;
|
import com.tashow.erp.service.BanmaOrderService;
|
||||||
@@ -15,6 +17,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
|
|||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -25,13 +28,15 @@ import java.util.stream.Collectors;
|
|||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 斑马订单服务实现
|
||||||
|
* 负责订单采集、物流查询和数据管理
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class BanmaOrderServiceImpl implements BanmaOrderService {
|
public class BanmaOrderServiceImpl implements BanmaOrderService {
|
||||||
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderServiceImpl.class);
|
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderServiceImpl.class);
|
||||||
private static final String API_URL = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d";
|
|
||||||
private static final String API_URL_WITH_TIME = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d";
|
|
||||||
private static final String TRACKING_URL = "https://banma365.cn/zebraExpressHub/web/tracking/getByExpressNumber/%s";
|
|
||||||
|
|
||||||
@Value("${api.server.base-url}")
|
@Value("${api.server.base-url}")
|
||||||
private String ruoyiAdminBase;
|
private String ruoyiAdminBase;
|
||||||
private RestTemplate restTemplate;
|
private RestTemplate restTemplate;
|
||||||
@@ -46,25 +51,30 @@ public class BanmaOrderServiceImpl implements BanmaOrderService {
|
|||||||
private String currentBatchSessionId = null;
|
private String currentBatchSessionId = null;
|
||||||
// 物流信息缓存,避免重复查询
|
// 物流信息缓存,避免重复查询
|
||||||
private final Map<String, String> trackingInfoCache = new ConcurrentHashMap<>();
|
private final Map<String, String> trackingInfoCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public BanmaOrderServiceImpl(BanmaOrderRepository banmaOrderRepository, CacheService cacheService, DataReportUtil dataReportUtil, ErrorReporter errorReporter) {
|
public BanmaOrderServiceImpl(BanmaOrderRepository banmaOrderRepository, CacheService cacheService, DataReportUtil dataReportUtil, ErrorReporter errorReporter) {
|
||||||
this.banmaOrderRepository = banmaOrderRepository;
|
this.banmaOrderRepository = banmaOrderRepository;
|
||||||
this.cacheService = cacheService;
|
this.cacheService = cacheService;
|
||||||
this.dataReportUtil = dataReportUtil;
|
this.dataReportUtil = dataReportUtil;
|
||||||
this.errorReporter = errorReporter;
|
this.errorReporter = errorReporter;
|
||||||
RestTemplateBuilder builder = new RestTemplateBuilder();
|
RestTemplateBuilder builder = new RestTemplateBuilder();
|
||||||
builder.connectTimeout(Duration.ofSeconds(5));
|
builder.connectTimeout(Duration.ofSeconds(BanmaConstants.CONNECT_TIMEOUT_SECONDS));
|
||||||
builder.readTimeout(Duration.ofSeconds(10));
|
builder.readTimeout(Duration.ofSeconds(BanmaConstants.READ_TIMEOUT_SECONDS));
|
||||||
restTemplate = builder.build();
|
restTemplate = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从服务器获取认证Token
|
||||||
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private void fetchTokenFromServer(Long accountId) {
|
private void fetchTokenFromServer(Long accountId) {
|
||||||
Map<String, Object> resp = restTemplate.getForObject(ruoyiAdminBase + "/tool/banma/accounts", Map.class);
|
Map<String, Object> resp = restTemplate.getForObject(ruoyiAdminBase + "/tool/banma/accounts", Map.class);
|
||||||
List<Map<String, Object>> list = (List<Map<String, Object>>) resp.get("data");
|
List<Map<String, Object>> list = (List<Map<String, Object>>) resp.get("data");
|
||||||
if (list == null || list.isEmpty()) return;
|
if (list == null || list.isEmpty()) return;
|
||||||
Map<String, Object> account = accountId != null
|
Map<String, Object> account = accountId != null
|
||||||
? list.stream().filter(m -> accountId.equals(((Number) m.get("id")).longValue())).findFirst().orElse(null)
|
? list.stream().filter(m -> accountId.equals(((Number) m.get("id")).longValue())).findFirst().orElse(null)
|
||||||
: list.stream().filter(m -> ((Number) m.getOrDefault("isDefault", 0)).intValue() == 1).findFirst().orElse(list.get(0));
|
: list.stream().filter(m -> ((Number) m.getOrDefault("isDefault", 0)).intValue() == 1).findFirst().orElse(list.get(0));
|
||||||
|
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
String token = (String) account.get("token");
|
String token = (String) account.get("token");
|
||||||
currentAuthToken = token != null && token.startsWith("Bearer ") ? token : "Bearer " + token;
|
currentAuthToken = token != null && token.startsWith("Bearer ") ? token : "Bearer " + token;
|
||||||
@@ -82,7 +92,7 @@ public class BanmaOrderServiceImpl implements BanmaOrderService {
|
|||||||
if (currentAuthToken != null) headers.set("Authorization", currentAuthToken);
|
if (currentAuthToken != null) headers.set("Authorization", currentAuthToken);
|
||||||
HttpEntity<String> httpEntity = new HttpEntity<>(headers);
|
HttpEntity<String> httpEntity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
String url = "https://banma365.cn/api/shop/list?_t=" + System.currentTimeMillis();
|
String url = String.format(BanmaConstants.API_SHOP_LIST, System.currentTimeMillis());
|
||||||
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class);
|
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class);
|
||||||
|
|
||||||
return response.getBody() != null ? response.getBody() : new HashMap<>();
|
return response.getBody() != null ? response.getBody() : new HashMap<>();
|
||||||
@@ -105,16 +115,16 @@ public class BanmaOrderServiceImpl implements BanmaOrderService {
|
|||||||
String shopIdsParam = "";
|
String shopIdsParam = "";
|
||||||
if (shopIds != null && !shopIds.isEmpty()) {
|
if (shopIds != null && !shopIds.isEmpty()) {
|
||||||
List<String> validShopIds = shopIds.stream()
|
List<String> validShopIds = shopIds.stream()
|
||||||
.filter(id -> id != null && !id.trim().isEmpty())
|
.filter(id -> id != null && !id.trim().isEmpty())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (!validShopIds.isEmpty()) {
|
if (!validShopIds.isEmpty()) {
|
||||||
shopIdsParam = "shopIds[]=" + String.join("&shopIds[]=", validShopIds) + "&";
|
shopIdsParam = "shopIds[]=" + String.join("&shopIds[]=", validShopIds) + "&";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String url = (StringUtils.isEmpty(startDate) || StringUtils.isEmpty(endDate))
|
String url = (StringUtils.isEmpty(startDate) || StringUtils.isEmpty(endDate))
|
||||||
? String.format(API_URL, shopIdsParam, page, pageSize, System.currentTimeMillis())
|
? String.format(BanmaConstants.API_ORDER_LIST, shopIdsParam, page, pageSize, System.currentTimeMillis())
|
||||||
: String.format(API_URL_WITH_TIME, shopIdsParam, page, pageSize, startDate, endDate, System.currentTimeMillis());
|
: String.format(BanmaConstants.API_ORDER_LIST_WITH_TIME, shopIdsParam, page, pageSize, startDate, endDate, System.currentTimeMillis());
|
||||||
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class);
|
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class);
|
||||||
if (response.getBody() == null || !Integer.valueOf(0).equals(response.getBody().get("code"))) {
|
if (response.getBody() == null || !Integer.valueOf(0).equals(response.getBody().get("code"))) {
|
||||||
Map<String, Object> errorResult = new HashMap<>();
|
Map<String, Object> errorResult = new HashMap<>();
|
||||||
@@ -127,14 +137,14 @@ public class BanmaOrderServiceImpl implements BanmaOrderService {
|
|||||||
int total = ((Number) dataMap.getOrDefault("total", 0)).intValue();
|
int total = ((Number) dataMap.getOrDefault("total", 0)).intValue();
|
||||||
|
|
||||||
List orders = Optional.ofNullable(dataMap.get("list"))
|
List orders = Optional.ofNullable(dataMap.get("list"))
|
||||||
.map(list -> (List<Map<String, Object>>) list)
|
.map(list -> (List<Map<String, Object>>) list)
|
||||||
.orElse(Collections.emptyList())
|
.orElse(Collections.emptyList())
|
||||||
.stream()
|
.stream()
|
||||||
.map(this::processOrderData)
|
.map(this::processOrderData)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
if (!orders.isEmpty()) dataReportUtil.reportDataCollection("BANMA", orders.size(), "0");
|
if (!orders.isEmpty()) dataReportUtil.reportDataCollection(BanmaConstants.DATA_TYPE, orders.size(), "0");
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("orders", orders);
|
result.put("orders", orders);
|
||||||
@@ -152,89 +162,92 @@ public class BanmaOrderServiceImpl implements BanmaOrderService {
|
|||||||
*/
|
*/
|
||||||
private Map processOrderData(Map<String, Object> order) {
|
private Map processOrderData(Map<String, Object> order) {
|
||||||
String trackingNumber = (String) order.get("internationalTrackingNumber");
|
String trackingNumber = (String) order.get("internationalTrackingNumber");
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
if (StringUtils.isNotEmpty(trackingNumber)) {
|
if (StringUtils.isNotEmpty(trackingNumber)) {
|
||||||
LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1);
|
LocalDateTime cutoffTime = LocalDateTime.now().minusHours(BanmaConstants.CACHE_HOURS);
|
||||||
if (banmaOrderRepository.existsByTrackingNumberAndCreatedAtAfter(trackingNumber, cutoffTime)) {
|
if (banmaOrderRepository.existsByTrackingNumberAndCreatedAtAfter(trackingNumber, cutoffTime)) {
|
||||||
return banmaOrderRepository.findLatestByTrackingNumber(trackingNumber)
|
return banmaOrderRepository.findLatestByTrackingNumber(trackingNumber)
|
||||||
.map(entity -> {
|
.map(entity -> {
|
||||||
if (currentBatchSessionId != null && !currentBatchSessionId.equals(entity.getSessionId())) {
|
if (currentBatchSessionId != null && !currentBatchSessionId.equals(entity.getSessionId())) {
|
||||||
entity.setSessionId(currentBatchSessionId);
|
entity.setSessionId(currentBatchSessionId);
|
||||||
entity.setCreatedAt(LocalDateTime.now());
|
entity.setUpdatedAt(LocalDateTime.now());
|
||||||
entity.setUpdatedAt(LocalDateTime.now());
|
banmaOrderRepository.save(entity);
|
||||||
banmaOrderRepository.save(entity);
|
}
|
||||||
}
|
try {
|
||||||
try {
|
return objectMapper.readValue(entity.getOrderData(), Map.class);
|
||||||
return objectMapper.readValue(entity.getOrderData(), Map.class);
|
} catch (Exception e) {
|
||||||
} catch (Exception e) {
|
logger.warn("解析缓存订单数据失败: {}", trackingNumber);
|
||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
} else {
|
} else {
|
||||||
banmaOrderRepository.findByTrackingNumber(trackingNumber)
|
banmaOrderRepository.findByTrackingNumber(trackingNumber).ifPresent(banmaOrderRepository::delete);
|
||||||
.ifPresent(banmaOrderRepository::delete);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建新订单数据
|
// 构建新订单
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("internationalTrackingNumber", trackingNumber);
|
result.put("internationalTrackingNumber", trackingNumber);
|
||||||
result.put("internationalShippingFee", order.get("internationalShippingFee"));
|
result.put("internationalShippingFee", order.get("internationalShippingFee"));
|
||||||
result.put("trackInfo", trackingInfoCache.computeIfAbsent(trackingNumber, this::fetchTrackingInfo));
|
result.put("trackInfo", trackingInfoCache.computeIfAbsent(trackingNumber, this::fetchTrackingInfo));
|
||||||
|
|
||||||
Optional.ofNullable(order.get("subOrders"))
|
Optional.ofNullable(order.get("subOrders"))
|
||||||
.map(sub -> (List<Map<String, Object>>) sub)
|
.map(sub -> (List<Map<String, Object>>) sub)
|
||||||
.filter(list -> !list.isEmpty())
|
.filter(list -> !list.isEmpty())
|
||||||
.map(list -> list.get(0))
|
.map(list -> list.get(0))
|
||||||
.ifPresent(subOrder -> extractSubOrderFields(result, subOrder));
|
.ifPresent(subOrder -> extractSubOrderFields(result, subOrder));
|
||||||
|
|
||||||
BanmaOrderEntity entity = new BanmaOrderEntity();
|
|
||||||
String entityTrackingNumber = (String) result.get("internationalTrackingNumber");
|
String entityTrackingNumber = (String) result.get("internationalTrackingNumber");
|
||||||
String shopOrderNumber = (String) result.get("shopOrderNumber");
|
String shopOrderNumber = (String) result.get("shopOrderNumber");
|
||||||
String productTitle = (String) result.get("productTitle");
|
String productTitle = (String) result.get("productTitle");
|
||||||
|
String orderId = String.valueOf(result.get("id"));
|
||||||
|
|
||||||
// 检查并上报空数据
|
// 上报空数据
|
||||||
if (StringUtils.isEmpty(entityTrackingNumber)) errorReporter.reportDataEmpty("banma", String.valueOf(result.get("id")), entityTrackingNumber);
|
if (StringUtils.isEmpty(entityTrackingNumber))
|
||||||
if (StringUtils.isEmpty(shopOrderNumber)) errorReporter.reportDataEmpty("banma", String.valueOf(result.get("id")), shopOrderNumber);
|
errorReporter.reportDataEmpty(BanmaConstants.DATA_TYPE.toLowerCase(), orderId, entityTrackingNumber);
|
||||||
if (StringUtils.isEmpty(productTitle)) errorReporter.reportDataEmpty("banma", String.valueOf(result.get("id")), productTitle);
|
if (StringUtils.isEmpty(shopOrderNumber))
|
||||||
|
errorReporter.reportDataEmpty(BanmaConstants.DATA_TYPE.toLowerCase(), orderId, shopOrderNumber);
|
||||||
|
if (StringUtils.isEmpty(productTitle))
|
||||||
|
errorReporter.reportDataEmpty(BanmaConstants.DATA_TYPE.toLowerCase(), orderId, productTitle);
|
||||||
|
|
||||||
|
// 生成跟踪号
|
||||||
if (StringUtils.isEmpty(entityTrackingNumber)) {
|
if (StringUtils.isEmpty(entityTrackingNumber)) {
|
||||||
if (StringUtils.isNotEmpty(shopOrderNumber)) {
|
if (StringUtils.isNotEmpty(shopOrderNumber)) {
|
||||||
entityTrackingNumber = "ORDER_" + shopOrderNumber;
|
entityTrackingNumber = BanmaConstants.TRACKING_PREFIX_ORDER + shopOrderNumber;
|
||||||
} else if (StringUtils.isNotEmpty(productTitle)) {
|
} else if (StringUtils.isNotEmpty(productTitle)) {
|
||||||
entityTrackingNumber = "PRODUCT_" + Math.abs(productTitle.hashCode());
|
entityTrackingNumber = BanmaConstants.TRACKING_PREFIX_PRODUCT + Math.abs(productTitle.hashCode());
|
||||||
} else {
|
} else {
|
||||||
entityTrackingNumber = "UNKNOWN_" + System.currentTimeMillis();
|
entityTrackingNumber = BanmaConstants.TRACKING_PREFIX_UNKNOWN + System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.setTrackingNumber(entityTrackingNumber);
|
|
||||||
try {
|
|
||||||
entity.setOrderData(objectMapper.writeValueAsString(result));
|
|
||||||
} catch (Exception e) {
|
|
||||||
entity.setOrderData("{}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成会话ID
|
// 生成会话ID
|
||||||
String sessionId = currentBatchSessionId != null ? currentBatchSessionId :
|
String sessionId = currentBatchSessionId != null ? currentBatchSessionId :
|
||||||
Optional.ofNullable((String) result.get("orderedAt"))
|
Optional.ofNullable((String) result.get("orderedAt"))
|
||||||
.filter(orderedAt -> orderedAt.length() >= 10)
|
.filter(orderedAt -> orderedAt.length() >= 10)
|
||||||
.map(orderedAt -> "SESSION_" + orderedAt.substring(0, 10))
|
.map(orderedAt -> BanmaConstants.SESSION_PREFIX + orderedAt.substring(0, 10))
|
||||||
.orElse("SESSION_" + java.time.LocalDate.now().toString());
|
.orElse(BanmaConstants.SESSION_PREFIX + java.time.LocalDate.now());
|
||||||
|
|
||||||
|
// 保存实体
|
||||||
|
BanmaOrderEntity entity = new BanmaOrderEntity();
|
||||||
|
entity.setTrackingNumber(entityTrackingNumber);
|
||||||
entity.setSessionId(sessionId);
|
entity.setSessionId(sessionId);
|
||||||
entity.setCreatedAt(LocalDateTime.now());
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
entity.setUpdatedAt(LocalDateTime.now());
|
entity.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
entity.setOrderData(objectMapper.writeValueAsString(result));
|
||||||
banmaOrderRepository.save(entity);
|
banmaOrderRepository.save(entity);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("保存订单数据失败,跳过: {}", entityTrackingNumber);
|
logger.warn("保存订单失败: {}", entityTrackingNumber, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取子订单字段
|
||||||
|
*/
|
||||||
private void extractSubOrderFields(Map<String, Object> simplifiedOrder, Map<String, Object> subOrder) {
|
private void extractSubOrderFields(Map<String, Object> simplifiedOrder, Map<String, Object> subOrder) {
|
||||||
String[] basicFields = {"orderedAt", "timeSinceOrder", "createdAt", "poTrackingNumber"};
|
String[] basicFields = {"orderedAt", "timeSinceOrder", "createdAt", "poTrackingNumber"};
|
||||||
String[] productFields = {"productTitle", "shopOrderNumber", "priceJpy", "productQuantity", "shippingFeeJpy", "productNumber", "serviceFee", "productImage"};
|
String[] productFields = {"productTitle", "shopOrderNumber", "priceJpy", "productQuantity", "shippingFeeJpy", "productNumber", "serviceFee", "productImage"};
|
||||||
@@ -244,18 +257,22 @@ public class BanmaOrderServiceImpl implements BanmaOrderService {
|
|||||||
Arrays.stream(purchaseFields).forEach(field -> simplifiedOrder.put(field, subOrder.get(field)));
|
Arrays.stream(purchaseFields).forEach(field -> simplifiedOrder.put(field, subOrder.get(field)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取物流信息
|
||||||
|
*/
|
||||||
private String fetchTrackingInfo(String trackingNumber) {
|
private String fetchTrackingInfo(String trackingNumber) {
|
||||||
Map<String, Object> trackInfoMap = (Map<String, Object>) new SagawaExpressSdk().getTrackingInfo(trackingNumber).get("trackInfo");
|
Map<String, Object> trackInfoMap = (Map<String, Object>) new SagawaExpressSdk().getTrackingInfo(trackingNumber).get("trackInfo");
|
||||||
if (trackInfoMap != null) {
|
if (trackInfoMap != null) {
|
||||||
return trackInfoMap.get("dateTime") + " " + trackInfoMap.get("office") + " " + trackInfoMap.get("status");
|
return trackInfoMap.get("dateTime") + " " + trackInfoMap.get("office") + " " + trackInfoMap.get("status");
|
||||||
}
|
}
|
||||||
ResponseEntity<Map> response = restTemplate.getForEntity(String.format(TRACKING_URL, trackingNumber), Map.class);
|
ResponseEntity<Map> response = restTemplate.getForEntity(String.format(BanmaConstants.API_TRACKING, trackingNumber), Map.class);
|
||||||
return Optional.ofNullable(response.getBody())
|
return Optional.ofNullable(response.getBody())
|
||||||
.filter(body -> Integer.valueOf(0).equals(body.get("code")))
|
.filter(body -> Integer.valueOf(0).equals(body.get("code")))
|
||||||
.map(body -> (List<Map<String, Object>>) body.get("data"))
|
.map(body -> (List<Map<String, Object>>) body.get("data"))
|
||||||
.filter(list -> !list.isEmpty())
|
.filter(list -> !list.isEmpty())
|
||||||
.map(list -> list.get(0))
|
.map(list -> list.get(0))
|
||||||
.map(track -> (String) track.get("track"))
|
.map(track -> (String) track.get("track"))
|
||||||
.orElse("暂无物流信息");
|
.orElse("暂无物流信息");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.tashow.erp.service.impl;
|
package com.tashow.erp.service.impl;
|
||||||
|
|
||||||
|
import com.tashow.erp.common.CacheConstants;
|
||||||
import com.tashow.erp.entity.BrandTrademarkCacheEntity;
|
import com.tashow.erp.entity.BrandTrademarkCacheEntity;
|
||||||
import com.tashow.erp.repository.BrandTrademarkCacheRepository;
|
import com.tashow.erp.repository.BrandTrademarkCacheRepository;
|
||||||
import com.tashow.erp.service.BrandTrademarkCacheService;
|
import com.tashow.erp.service.BrandTrademarkCacheService;
|
||||||
@@ -7,50 +8,66 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 品牌商标缓存服务实现
|
||||||
|
* 提供商标查询结果的缓存管理
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheService {
|
public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BrandTrademarkCacheRepository repository;
|
private BrandTrademarkCacheRepository repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从缓存获取品牌商标注册状态
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Boolean> getCached(List<String> brands) {
|
public Map<String, Boolean> getCached(List<String> brands) {
|
||||||
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
|
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(CacheConstants.TRADEMARK_CACHE_DAYS);
|
||||||
List<BrandTrademarkCacheEntity> cached = repository.findByBrandInAndCreatedAtAfter(brands, cutoffTime);
|
List<BrandTrademarkCacheEntity> cached = repository.findByBrandInAndCreatedAtAfter(brands, cutoffTime);
|
||||||
|
|
||||||
Map<String, Boolean> result = new HashMap<>();
|
Map<String, Boolean> result = new HashMap<>();
|
||||||
cached.forEach(e -> result.put(e.getBrand(), e.getRegistered()));
|
cached.forEach(e -> result.put(e.getBrand(), e.getRegistered()));
|
||||||
|
|
||||||
if (!result.isEmpty()) {
|
|
||||||
log.info("从全局缓存获取 {} 个品牌数据", result.size());
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存商标查询结果到缓存
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void saveResults(Map<String, Boolean> results) {
|
public void saveResults(Map<String, Boolean> results) {
|
||||||
|
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(CacheConstants.TRADEMARK_CACHE_DAYS);
|
||||||
results.forEach((brand, registered) -> {
|
results.forEach((brand, registered) -> {
|
||||||
if (!repository.existsByBrand(brand)) {
|
repository.findByBrandAndCreatedAtAfter(brand, cutoffTime)
|
||||||
BrandTrademarkCacheEntity entity = new BrandTrademarkCacheEntity();
|
.ifPresentOrElse(
|
||||||
entity.setBrand(brand);
|
existing -> {
|
||||||
entity.setRegistered(registered);
|
existing.setRegistered(registered);
|
||||||
repository.save(entity);
|
existing.setCreatedAt(LocalDateTime.now());
|
||||||
}
|
repository.save(existing);
|
||||||
|
},
|
||||||
|
() -> {
|
||||||
|
BrandTrademarkCacheEntity entity = new BrandTrademarkCacheEntity();
|
||||||
|
entity.setBrand(brand);
|
||||||
|
entity.setRegistered(registered);
|
||||||
|
repository.save(entity);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期缓存数据
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void cleanExpired() {
|
public void cleanExpired() {
|
||||||
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
|
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(CacheConstants.TRADEMARK_CACHE_DAYS);
|
||||||
repository.deleteByCreatedAtBefore(cutoffTime);
|
repository.deleteByCreatedAtBefore(cutoffTime);
|
||||||
log.info("清理1天前的品牌商标缓存");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.tashow.erp.service.impl;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.tashow.erp.common.FangzhouConstants;
|
||||||
import com.tashow.erp.service.IFangzhouApiService;
|
import com.tashow.erp.service.IFangzhouApiService;
|
||||||
import com.tashow.erp.utils.ApiForwarder;
|
import com.tashow.erp.utils.ApiForwarder;
|
||||||
import com.tashow.erp.utils.LoggerUtil;
|
import com.tashow.erp.utils.LoggerUtil;
|
||||||
@@ -14,6 +15,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.ResourceAccessException;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -22,14 +24,12 @@ import java.security.MessageDigest;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 方舟精选 API 服务实现
|
* 方舟精选API服务实现
|
||||||
|
* 负责与方舟精选平台的API交互
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
||||||
private static final Logger logger = LoggerUtil.getLogger(FangzhouApiServiceImpl.class);
|
private static final Logger logger = LoggerUtil.getLogger(FangzhouApiServiceImpl.class);
|
||||||
private static final String API_SECRET = "e10adc3949ba59abbe56e057f20f883e";
|
|
||||||
private static final String FANGZHOU_API_URL = "https://api.fangzhoujingxuan.com/Task";
|
|
||||||
private static final int TOKEN_EXPIRED_CODE = -1006;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RestTemplate restTemplate;
|
private RestTemplate restTemplate;
|
||||||
@@ -40,6 +40,9 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ApiForwarder apiForwarder;
|
private ApiForwarder apiForwarder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取API Token
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String getToken() {
|
public String getToken() {
|
||||||
try {
|
try {
|
||||||
@@ -59,6 +62,9 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新API Token
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String refreshToken() {
|
public String refreshToken() {
|
||||||
try {
|
try {
|
||||||
@@ -77,6 +83,9 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用方舟精选API
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public JsonNode callApi(String command, String data, String token) {
|
public JsonNode callApi(String command, String data, String token) {
|
||||||
try {
|
try {
|
||||||
@@ -86,35 +95,60 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
|||||||
formData.add("c", command);
|
formData.add("c", command);
|
||||||
formData.add("d", data);
|
formData.add("d", data);
|
||||||
formData.add("t", token);
|
formData.add("t", token);
|
||||||
formData.add("s", md5(ts + data + API_SECRET));
|
formData.add("s", md5(ts + data + FangzhouConstants.API_SECRET));
|
||||||
formData.add("ts", String.valueOf(ts));
|
formData.add("ts", String.valueOf(ts));
|
||||||
formData.add("website", "1");
|
formData.add("website", FangzhouConstants.WEBSITE_CODE);
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
|
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
|
||||||
|
|
||||||
String result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class);
|
String result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class);
|
||||||
JsonNode json = objectMapper.readTree(result);
|
JsonNode json = objectMapper.readTree(result);
|
||||||
|
|
||||||
// 处理 Token 过期,自动刷新重试
|
// 处理 Token 失效
|
||||||
if (json.get("S").asInt() == TOKEN_EXPIRED_CODE) {
|
int statusCode = json.get("S").asInt();
|
||||||
logger.info("Token 过期,刷新后重试");
|
if (statusCode == FangzhouConstants.TOKEN_EXPIRED_CODE || statusCode == -1002) {
|
||||||
String newToken = refreshToken();
|
String newToken = statusCode == -1002 ? getToken() : refreshToken();
|
||||||
|
logger.info("Token 失效({}), {}后重试", statusCode, statusCode == -1002 ? "重新注册" : "刷新");
|
||||||
formData.set("t", newToken);
|
formData.set("t", newToken);
|
||||||
formData.set("s", md5(ts + data + API_SECRET));
|
formData.set("s", md5(ts + data + FangzhouConstants.API_SECRET));
|
||||||
requestEntity = new HttpEntity<>(formData, headers);
|
requestEntity = new HttpEntity<>(formData, headers);
|
||||||
result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class);
|
result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class);
|
||||||
json = objectMapper.readTree(result);
|
json = objectMapper.readTree(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json;
|
return json;
|
||||||
|
} catch (ResourceAccessException e) {
|
||||||
|
logger.warn("证书验证失败,尝试重新注册: {}", e.getMessage());
|
||||||
|
String newToken = refreshToken();
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||||
|
formData.add("c", command);
|
||||||
|
formData.add("d", data);
|
||||||
|
formData.add("t", newToken);
|
||||||
|
formData.add("s", md5(ts + data + FangzhouConstants.API_SECRET));
|
||||||
|
formData.add("ts", String.valueOf(ts));
|
||||||
|
formData.add("website", FangzhouConstants.WEBSITE_CODE);
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
|
||||||
|
try {
|
||||||
|
String result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class);
|
||||||
|
return objectMapper.readTree(result);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("重试失败", ex);
|
||||||
|
throw new RuntimeException("调用 API 失败: " + ex.getMessage());
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("调用方舟精选 API 失败", e);
|
logger.error("调用方舟精选 API 失败", e);
|
||||||
throw new RuntimeException("调用 API 失败: " + e.getMessage());
|
throw new RuntimeException("调用 API 失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到方舟精选
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public JsonNode uploadFile(MultipartFile file, String token) {
|
public JsonNode uploadFile(MultipartFile file, String token) {
|
||||||
try {
|
try {
|
||||||
@@ -126,29 +160,53 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
|||||||
formData.add("t", token);
|
formData.add("t", token);
|
||||||
formData.add("ts", ts);
|
formData.add("ts", ts);
|
||||||
formData.add("d", data);
|
formData.add("d", data);
|
||||||
formData.add("s", md5(ts + data + API_SECRET));
|
formData.add("s", md5(ts + data + FangzhouConstants.API_SECRET));
|
||||||
formData.add("website", "1");
|
formData.add("website", FangzhouConstants.WEBSITE_CODE);
|
||||||
formData.add("files", file.getResource());
|
formData.add("files", file.getResource());
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
|
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
|
||||||
|
|
||||||
String result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class);
|
String result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class);
|
||||||
JsonNode json = objectMapper.readTree(result);
|
JsonNode json = objectMapper.readTree(result);
|
||||||
|
|
||||||
// 处理 Token 过期
|
// 处理 Token 失效
|
||||||
if (json.get("S").asInt() == TOKEN_EXPIRED_CODE) {
|
int statusCode = json.get("S").asInt();
|
||||||
logger.info("Token 过期,刷新后重试");
|
if (statusCode == FangzhouConstants.TOKEN_EXPIRED_CODE || statusCode == -1002) {
|
||||||
String newToken = refreshToken();
|
String newToken = statusCode == -1002 ? getToken() : refreshToken();
|
||||||
|
logger.info("Token 失效({}), {}后重试", statusCode, statusCode == -1002 ? "重新注册" : "刷新");
|
||||||
formData.set("t", newToken);
|
formData.set("t", newToken);
|
||||||
formData.set("s", md5(ts + data + API_SECRET));
|
formData.set("s", md5(ts + data + FangzhouConstants.API_SECRET));
|
||||||
requestEntity = new HttpEntity<>(formData, headers);
|
requestEntity = new HttpEntity<>(formData, headers);
|
||||||
result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class);
|
result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class);
|
||||||
json = objectMapper.readTree(result);
|
json = objectMapper.readTree(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json;
|
return json;
|
||||||
|
} catch (ResourceAccessException e) {
|
||||||
|
logger.warn("证书验证失败,尝试重新注册: {}", e.getMessage());
|
||||||
|
String newToken = refreshToken();
|
||||||
|
String data = String.format("{\"name\":\"%s\",\"type\":1}", file.getOriginalFilename());
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
|
||||||
|
formData.add("c", "Create");
|
||||||
|
formData.add("t", newToken);
|
||||||
|
formData.add("ts", ts);
|
||||||
|
formData.add("d", data);
|
||||||
|
formData.add("s", md5(ts + data + FangzhouConstants.API_SECRET));
|
||||||
|
formData.add("website", FangzhouConstants.WEBSITE_CODE);
|
||||||
|
formData.add("files", file.getResource());
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||||
|
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
|
||||||
|
try {
|
||||||
|
String result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class);
|
||||||
|
return objectMapper.readTree(result);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("重试失败", ex);
|
||||||
|
throw new RuntimeException("上传文件失败: " + ex.getMessage());
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("上传文件失败", e);
|
logger.error("上传文件失败", e);
|
||||||
throw new RuntimeException("上传文件失败: " + e.getMessage());
|
throw new RuntimeException("上传文件失败: " + e.getMessage());
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import java.net.URL;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟卖精灵服务实现
|
||||||
|
* 负责打开和操作跟卖精灵网站
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class GenmaiServiceImpl {
|
public class GenmaiServiceImpl {
|
||||||
@Value("${api.server.base-url}")
|
@Value("${api.server.base-url}")
|
||||||
@@ -21,6 +25,9 @@ public class GenmaiServiceImpl {
|
|||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开跟卖精灵网站
|
||||||
|
*/
|
||||||
public void openGenmaiWebsite(Long accountId, String username) throws Exception {
|
public void openGenmaiWebsite(Long accountId, String username) throws Exception {
|
||||||
WebDriverManager.chromedriver()
|
WebDriverManager.chromedriver()
|
||||||
.driverRepositoryUrl(new URL("https://registry.npmmirror.com/-/binary/chromedriver/"))
|
.driverRepositoryUrl(new URL("https://registry.npmmirror.com/-/binary/chromedriver/"))
|
||||||
@@ -46,6 +53,9 @@ public class GenmaiServiceImpl {
|
|||||||
driver.navigate().refresh();
|
driver.navigate().refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取并验证Token
|
||||||
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private String getAndValidateToken(Long accountId, String username) {
|
private String getAndValidateToken(Long accountId, String username) {
|
||||||
String url = serverApiUrl + "/tool/genmai/accounts?name=" + username;
|
String url = serverApiUrl + "/tool/genmai/accounts?name=" + username;
|
||||||
@@ -68,6 +78,9 @@ public class GenmaiServiceImpl {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证Token是否有效
|
||||||
|
*/
|
||||||
private boolean validateToken(String token) {
|
private boolean validateToken(String token) {
|
||||||
try {
|
try {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
package com.tashow.erp.service.impl;
|
package com.tashow.erp.service.impl;
|
||||||
|
|
||||||
|
import com.tashow.erp.common.CacheConstants;
|
||||||
import com.tashow.erp.entity.RakutenProductEntity;
|
import com.tashow.erp.entity.RakutenProductEntity;
|
||||||
import com.tashow.erp.model.RakutenProduct;
|
import com.tashow.erp.model.RakutenProduct;
|
||||||
import com.tashow.erp.repository.RakutenProductRepository;
|
import com.tashow.erp.repository.RakutenProductRepository;
|
||||||
@@ -61,10 +63,10 @@ public class RakutenCacheServiceImpl implements RakutenCacheService {
|
|||||||
.filter(name -> name != null && !name.trim().isEmpty())
|
.filter(name -> name != null && !name.trim().isEmpty())
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
// 清理所有1小时前的旧数据,不分店铺全部清掉
|
// 清理所有过期的旧数据,不分店铺全部清掉
|
||||||
LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1);
|
LocalDateTime cutoffTime = LocalDateTime.now().minusHours(CacheConstants.RAKUTEN_CACHE_HOURS);
|
||||||
repository.deleteAllDataBefore(cutoffTime);
|
repository.deleteAllDataBefore(cutoffTime);
|
||||||
log.info("清理1小时前的所有旧数据");
|
log.info("清理{}小时前的所有旧数据", CacheConstants.RAKUTEN_CACHE_HOURS);
|
||||||
|
|
||||||
List<RakutenProductEntity> entities = products.stream()
|
List<RakutenProductEntity> entities = products.stream()
|
||||||
.map(product -> {
|
.map(product -> {
|
||||||
@@ -80,7 +82,7 @@ public class RakutenCacheServiceImpl implements RakutenCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查店铺是否有1小时内的缓存数据(按用户隔离)
|
* 检查店铺是否有缓存时间内的缓存数据(按用户隔离)
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean hasRecentData(String shopName, String username) {
|
public boolean hasRecentData(String shopName, String username) {
|
||||||
@@ -88,9 +90,9 @@ public class RakutenCacheServiceImpl implements RakutenCacheService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
boolean hasRecent = repository.existsByOriginalShopNameAndSessionIdStartingWithAndCreatedAtAfter(
|
boolean hasRecent = repository.existsByOriginalShopNameAndSessionIdStartingWithAndCreatedAtAfter(
|
||||||
shopName, username + "#", LocalDateTime.now().minusHours(1));
|
shopName, username + "#", LocalDateTime.now().minusHours(CacheConstants.RAKUTEN_CACHE_HOURS));
|
||||||
if (hasRecent) {
|
if (hasRecent) {
|
||||||
log.info("店铺 {} 存在1小时内缓存数据(用户: {}),将使用缓存", shopName, username);
|
log.info("店铺 {} 存在缓存时间内缓存数据(用户: {}),将使用缓存", shopName, username);
|
||||||
}
|
}
|
||||||
return hasRecent;
|
return hasRecent;
|
||||||
}
|
}
|
||||||
@@ -114,6 +116,9 @@ public class RakutenCacheServiceImpl implements RakutenCacheService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新指定店铺的所有产品的会话ID
|
* 更新指定店铺的所有产品的会话ID
|
||||||
|
*
|
||||||
|
* @param shopName 店铺名
|
||||||
|
* @param newSessionId 新的会话ID
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -131,6 +136,9 @@ public class RakutenCacheServiceImpl implements RakutenCacheService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新指定产品列表的会话ID,只更新这些具体的产品
|
* 更新指定产品列表的会话ID,只更新这些具体的产品
|
||||||
|
*
|
||||||
|
* @param products 产品列表
|
||||||
|
* @param newSessionId 新的会话ID
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -167,7 +175,9 @@ public class RakutenCacheServiceImpl implements RakutenCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理指定店铺1小时之前的旧数据,保留1小时内的缓存
|
* 清理指定店铺的旧数据,保留1小时内的缓存
|
||||||
|
*
|
||||||
|
* @param shopName 店铺名
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -178,5 +188,4 @@ public class RakutenCacheServiceImpl implements RakutenCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,9 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
|
|||||||
return products;
|
return products;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐天页面解析器
|
||||||
|
*/
|
||||||
private class RakutenPageProcessor implements PageProcessor {
|
private class RakutenPageProcessor implements PageProcessor {
|
||||||
private final List<RakutenProduct> products;
|
private final List<RakutenProduct> products;
|
||||||
private final ErrorReporter errorReporter;
|
private final ErrorReporter errorReporter;
|
||||||
@@ -60,6 +63,9 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
|
|||||||
this.errorReporter = errorReporter;
|
this.errorReporter = errorReporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析乐天页面数据
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void process(Page page) {
|
public void process(Page page) {
|
||||||
List<String> rankings = page.getHtml().xpath("//div[@class='srhRnk']/span[@class='icon']/text()").all();
|
List<String> rankings = page.getHtml().xpath("//div[@class='srhRnk']/span[@class='icon']/text()").all();
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ package com.tashow.erp.test;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.tashow.erp.ErpClientSbApplication;
|
||||||
|
import com.tashow.erp.utils.TrademarkCheckUtil;
|
||||||
|
import org.openqa.selenium.JavascriptExecutor;
|
||||||
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
@@ -15,145 +21,210 @@ public class UsptoApiTest {
|
|||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.out.println("=== USPTO API 功能测试 ===\n");
|
System.out.println("=== 商标查询测试(使用Spring容器) ===\n");
|
||||||
System.out.println("API Key: " + API_KEY + "\n");
|
|
||||||
|
|
||||||
// 测试1:通过序列号查询(已知可用)
|
|
||||||
testBySerialNumber();
|
|
||||||
|
|
||||||
System.out.println("\n" + "=".repeat(60) + "\n");
|
|
||||||
|
|
||||||
// 测试2:尝试通过品牌名称搜索
|
|
||||||
testByBrandName();
|
|
||||||
|
|
||||||
System.out.println("\n" + "=".repeat(60) + "\n");
|
|
||||||
|
|
||||||
// 测试3:对比当前实现的tmsearch
|
|
||||||
testCurrentImplementation();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试1:通过序列号查询(官方TSDR API)
|
|
||||||
*/
|
|
||||||
private static void testBySerialNumber() {
|
|
||||||
System.out.println("【测试1】通过序列号查询");
|
|
||||||
System.out.println("端点: https://tsdrapi.uspto.gov/ts/cd/casestatus/sn88123456/info.json");
|
|
||||||
|
|
||||||
|
// 启动Spring Boot应用上下文
|
||||||
|
ConfigurableApplicationContext context = null;
|
||||||
try {
|
try {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
System.out.println("正在启动Spring Boot应用...");
|
||||||
headers.set("USPTO-API-KEY", API_KEY);
|
context = SpringApplication.run(ErpClientSbApplication.class, args);
|
||||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
System.out.println("Spring Boot应用启动成功!\n");
|
||||||
|
|
||||||
ResponseEntity<String> response = rest.exchange(
|
// 获取TrademarkCheckUtil Bean
|
||||||
"https://tsdrapi.uspto.gov/ts/cd/casestatus/sn88123456/info.json",
|
TrademarkCheckUtil trademarkUtil = context.getBean(TrademarkCheckUtil.class);
|
||||||
HttpMethod.GET, entity, String.class
|
|
||||||
);
|
|
||||||
|
|
||||||
JsonNode json = mapper.readTree(response.getBody());
|
// 测试单品牌查询(获取详细结果)
|
||||||
String markElement = json.get("trademarks").get(0).get("status").get("markElement").asText();
|
testSingleBrandWithDetailedResults("MADCKDEDRT");
|
||||||
int status = json.get("trademarks").get(0).get("status").get("status").asInt();
|
|
||||||
String regNumber = json.get("trademarks").get(0).get("status").get("usRegistrationNumber").asText();
|
|
||||||
|
|
||||||
System.out.println("✓ 成功!");
|
|
||||||
System.out.println(" 商标名称: " + markElement);
|
|
||||||
System.out.println(" 状态码: " + status);
|
|
||||||
System.out.println(" 注册号: " + (regNumber.isEmpty() ? "未注册" : regNumber));
|
|
||||||
System.out.println("\n结论: ✅ 可以查询,但必须知道序列号");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("✗ 失败: " + e.getMessage());
|
System.err.println("测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试2:尝试通过品牌名称搜索
|
* 标准化品牌名称用于比较(移除特殊字符,转换为小写)
|
||||||
*/
|
*/
|
||||||
private static void testByBrandName() {
|
private static String normalizeBrandName(String name) {
|
||||||
System.out.println("【测试2】尝试通过品牌名称搜索");
|
if (name == null) return "";
|
||||||
|
return name.toLowerCase()
|
||||||
String[] brands = {"Nike", "MYLIFE", "TestBrand123"};
|
.replaceAll("[\\s\\-_\\.]", "") // 移除空格、连字符、下划线、点号
|
||||||
String[] searchUrls = {
|
.replaceAll("[^a-z0-9]", ""); // 只保留字母和数字
|
||||||
"https://tsdrapi.uspto.gov/ts/cd/search?q=%s",
|
}
|
||||||
"https://tsdrapi.uspto.gov/search?keyword=%s",
|
|
||||||
"https://api.uspto.gov/trademark/search?q=%s",
|
/**
|
||||||
"https://api.uspto.gov/tmsearch/search?q=%s",
|
* 直接执行JavaScript获取详细结果
|
||||||
};
|
*
|
||||||
|
* USPTO API返回格式说明:
|
||||||
boolean foundSearchApi = false;
|
* {
|
||||||
|
* "hits": {
|
||||||
for (String brand : brands) {
|
* "hits": [
|
||||||
System.out.println("\n测试品牌: " + brand);
|
* {
|
||||||
|
* "id": "商标ID",
|
||||||
|
* "score": 匹配分数,
|
||||||
|
* "source": {
|
||||||
|
* "alive": true/false, // 关键字段:是否有效注册
|
||||||
|
* "wordmark": "品牌名", // 商标名称
|
||||||
|
* "statusCode": 状态码, // 600=已放弃, 700=已注册
|
||||||
|
* "statusDescription": "状态描述", // ABANDONED/REGISTERED
|
||||||
|
* "registrationId": "注册号", // 注册号(如果已注册)
|
||||||
|
* "registrationDate": "注册日期",
|
||||||
|
* "abandonDate": "放弃日期",
|
||||||
|
* "filedDate": "申请日期",
|
||||||
|
* "goodsAndServices": ["商品服务类别"],
|
||||||
|
* "internationalClass": ["IC 018"], // 国际分类
|
||||||
|
* "ownerName": ["所有者名称"],
|
||||||
|
* "attorney": "代理律师"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "totalValue": 总匹配数
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* 判定逻辑: 只要有任何一个记录的 alive=true,就判定为已注册(true)
|
||||||
|
*/
|
||||||
|
private static void testSingleBrandWithDetailedResults(String brandName) {
|
||||||
|
System.out.println("【单品牌测试】直接执行JavaScript获取详细结果");
|
||||||
|
System.out.println("品牌: " + brandName);
|
||||||
|
|
||||||
|
ChromeDriver driver = null;
|
||||||
|
try {
|
||||||
|
System.out.println("=== 开始测试品牌: " + brandName + " ===");
|
||||||
|
|
||||||
for (String urlTemplate : searchUrls) {
|
// 初始化Chrome驱动
|
||||||
String url = String.format(urlTemplate, brand);
|
System.out.println("正在初始化Chrome驱动...");
|
||||||
System.out.println(" 尝试: " + url);
|
driver = com.tashow.erp.utils.SeleniumUtil.createDriver(false, null);
|
||||||
|
driver.get("https://tmsearch.uspto.gov/search/search-results");
|
||||||
|
Thread.sleep(6000); // 等待页面加载
|
||||||
|
System.out.println("Chrome驱动初始化完成");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
String script = "const brands = ['" + brandName.replace("'", "\\'") + "'];\n" +
|
||||||
|
"const callback = arguments[arguments.length - 1];\n" +
|
||||||
|
"Promise.all(brands.map(b => \n" +
|
||||||
|
" fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', {\n" +
|
||||||
|
" method: 'POST',\n" +
|
||||||
|
" headers: {'Content-Type': 'application/json'},\n" +
|
||||||
|
" body: JSON.stringify({\n" +
|
||||||
|
" query: {bool: {must: [{bool: {should: [\n" +
|
||||||
|
" {match_phrase: {WM: {query: b, boost: 5}}},\n" +
|
||||||
|
" {match: {WM: {query: b, boost: 2}}},\n" +
|
||||||
|
" {match_phrase: {PM: {query: b, boost: 2}}}\n" +
|
||||||
|
" ]}}]}},\n" +
|
||||||
|
" size: 100\n" +
|
||||||
|
" })\n" +
|
||||||
|
" })\n" +
|
||||||
|
" .then(r => {\n" +
|
||||||
|
" if (!r.ok) {\n" +
|
||||||
|
" return {brand: b, error: 'HTTP ' + r.status + ': ' + r.statusText, allResults: []};\n" +
|
||||||
|
" }\n" +
|
||||||
|
" return r.json().then(d => {\n" +
|
||||||
|
" console.log('API Response:', d);\n" +
|
||||||
|
" return {\n" +
|
||||||
|
" brand: b,\n" +
|
||||||
|
" error: null,\n" +
|
||||||
|
" totalHits: d?.hits?.total?.value || d?.hits?.totalValue || 0,\n" +
|
||||||
|
" allResults: d?.hits?.hits || [],\n" +
|
||||||
|
" rawData: d\n" +
|
||||||
|
" };\n" +
|
||||||
|
" });\n" +
|
||||||
|
" })\n" +
|
||||||
|
" .catch(e => ({\n" +
|
||||||
|
" brand: b,\n" +
|
||||||
|
" error: e.name + ': ' + e.message,\n" +
|
||||||
|
" allResults: []\n" +
|
||||||
|
" }))\n" +
|
||||||
|
")).then(callback);";
|
||||||
|
|
||||||
|
System.out.println("正在执行JavaScript脚本...");
|
||||||
|
|
||||||
|
// 执行JavaScript脚本
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.List<java.util.Map<String, Object>> results =
|
||||||
|
(java.util.List<java.util.Map<String, Object>>)
|
||||||
|
((JavascriptExecutor) driver).executeAsyncScript(script);
|
||||||
|
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
// 极简输出 - 只返回 true/false 判定结果
|
||||||
|
boolean isRegistered = false;
|
||||||
|
|
||||||
|
if (results != null && !results.isEmpty()) {
|
||||||
|
java.util.Map<String, Object> result = results.get(0);
|
||||||
|
|
||||||
try {
|
// 获取所有匹配的结果
|
||||||
HttpHeaders headers = new HttpHeaders();
|
@SuppressWarnings("unchecked")
|
||||||
headers.set("USPTO-API-KEY", API_KEY);
|
java.util.List<java.util.Map<String, Object>> allResults =
|
||||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
(java.util.List<java.util.Map<String, Object>>) result.get("allResults");
|
||||||
|
|
||||||
ResponseEntity<String> response = rest.exchange(url, HttpMethod.GET, entity, String.class);
|
// 如果allResults为空,从rawData中获取
|
||||||
|
if ((allResults == null || allResults.isEmpty()) && result.get("rawData") != null) {
|
||||||
System.out.println(" ✓✓✓ 成功! 找到品牌搜索API!");
|
@SuppressWarnings("unchecked")
|
||||||
System.out.println(" 响应: " + response.getBody().substring(0, Math.min(200, response.getBody().length())));
|
java.util.Map<String, Object> rawData = (java.util.Map<String, Object>) result.get("rawData");
|
||||||
foundSearchApi = true;
|
@SuppressWarnings("unchecked")
|
||||||
break;
|
java.util.Map<String, Object> hits = (java.util.Map<String, Object>) rawData.get("hits");
|
||||||
} catch (Exception e) {
|
if (hits != null) {
|
||||||
System.out.println(" ✗ 404/失败");
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.List<java.util.Map<String, Object>> hitsArray =
|
||||||
|
(java.util.List<java.util.Map<String, Object>>) hits.get("hits");
|
||||||
|
if (hitsArray != null) {
|
||||||
|
allResults = hitsArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心判定逻辑:品牌名称匹配 且 statusCode为686/700 才判定为已注册
|
||||||
|
String normalizedInputBrand = normalizeBrandName(brandName);
|
||||||
|
if (allResults != null && !allResults.isEmpty()) {
|
||||||
|
for (java.util.Map<String, Object> hit : allResults) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.Map<String, Object> source = (java.util.Map<String, Object>) hit.get("_source");
|
||||||
|
if (source == null) {
|
||||||
|
source = (java.util.Map<String, Object>) hit.get("source");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source != null) {
|
||||||
|
String wordmark = (String) source.get("wordmark");
|
||||||
|
String normalizedWordmark = normalizeBrandName(wordmark);
|
||||||
|
|
||||||
|
// 首先检查品牌名称是否匹配
|
||||||
|
if (normalizedInputBrand.equals(normalizedWordmark)) {
|
||||||
|
Number statusCodeNum = (Number) source.get("statusCode");
|
||||||
|
|
||||||
|
// 只有statusCode为688或700才返回true
|
||||||
|
if (statusCodeNum != null && (statusCodeNum.intValue() == 688 || statusCodeNum.intValue() == 700)) {
|
||||||
|
isRegistered = true;
|
||||||
|
break; // 找到一个符合条件的就够了
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundSearchApi) break;
|
// 极简输出 - 只显示最终结果
|
||||||
}
|
System.out.println(isRegistered);
|
||||||
|
|
||||||
if (!foundSearchApi) {
|
|
||||||
System.out.println("\n结论: ❌ USPTO官方API不支持品牌名称搜索");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试3:当前实现的tmsearch方式
|
|
||||||
*/
|
|
||||||
private static void testCurrentImplementation() {
|
|
||||||
System.out.println("【测试3】当前实现方式(tmsearch内部API)");
|
|
||||||
System.out.println("端点: https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch");
|
|
||||||
|
|
||||||
String brand = "Nike";
|
|
||||||
String requestBody = String.format(
|
|
||||||
"{\"query\":{\"bool\":{\"must\":[{\"bool\":{\"should\":[" +
|
|
||||||
"{\"match_phrase\":{\"WM\":{\"query\":\"%s\",\"boost\":5}}}," +
|
|
||||||
"{\"match\":{\"WM\":{\"query\":\"%s\",\"boost\":2}}}," +
|
|
||||||
"{\"match_phrase\":{\"PM\":{\"query\":\"%s\",\"boost\":2}}}" +
|
|
||||||
"]}}]}},\"size\":1,\"_source\":[\"alive\"]}",
|
|
||||||
brand, brand, brand
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
|
|
||||||
|
|
||||||
ResponseEntity<String> response = rest.postForEntity(
|
|
||||||
"https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch",
|
|
||||||
entity, String.class
|
|
||||||
);
|
|
||||||
|
|
||||||
JsonNode json = mapper.readTree(response.getBody());
|
|
||||||
boolean hasResults = json.get("hits").get("hits").size() > 0;
|
|
||||||
|
|
||||||
System.out.println("✓ 成功!");
|
|
||||||
System.out.println(" 品牌: " + brand);
|
|
||||||
System.out.println(" 找到结果: " + (hasResults ? "是" : "否"));
|
|
||||||
|
|
||||||
if (hasResults) {
|
|
||||||
boolean alive = json.get("hits").get("hits").get(0).get("_source").get("alive").asBoolean();
|
|
||||||
System.out.println(" 是否注册: " + (alive ? "已注册" : "未注册"));
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println("\n结论: ✅ 支持品牌名称直接搜索(无需序列号)");
|
|
||||||
System.out.println(" ⚠️ 但这是内部API,非官方公开接口");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("✗ 失败: " + e.getMessage());
|
System.err.println("=== 测试失败 ===");
|
||||||
|
System.err.println("品牌: " + brandName);
|
||||||
|
System.err.println("错误: " + e.getMessage());
|
||||||
|
System.err.println("================");
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
// 确保清理资源
|
||||||
|
if (driver != null) {
|
||||||
|
try {
|
||||||
|
driver.quit();
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("清理Chrome驱动时出错: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ public class DataReportUtil {
|
|||||||
reportData.put("clientId", generateClientId(dataType));
|
reportData.put("clientId", generateClientId(dataType));
|
||||||
reportData.put("dataType", dataType);
|
reportData.put("dataType", dataType);
|
||||||
reportData.put("dataCount", dataCount);
|
reportData.put("dataCount", dataCount);
|
||||||
reportData.put("status", status);
|
reportData.put("status", status != null ? status : "0");
|
||||||
|
|
||||||
sendReportData(reportData);
|
sendReportData(reportData);
|
||||||
logger.debug("数据上报成功: {} - {} 条", dataType, dataCount);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("数据上报失败: {}", e.getMessage());
|
logger.warn("数据上报失败: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,17 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
import com.tashow.erp.service.impl.AuthServiceImpl;
|
import com.tashow.erp.service.impl.AuthServiceImpl;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class ErrorReporter {
|
public class ErrorReporter {
|
||||||
|
|
||||||
@Value("${api.server.base-url}")
|
|
||||||
private String serverUrl;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private AuthServiceImpl authService;
|
private AuthServiceImpl authService;
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
@Autowired
|
||||||
|
private ApiForwarder apiForwarder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上报启动失败错误
|
* 上报启动失败错误
|
||||||
@@ -67,17 +63,14 @@ public class ErrorReporter {
|
|||||||
errorData.put("errorType", errorType);
|
errorData.put("errorType", errorType);
|
||||||
errorData.put("errorMessage", errorMessage);
|
errorData.put("errorMessage", errorMessage);
|
||||||
errorData.put("stackTrace", getStackTrace(ex));
|
errorData.put("stackTrace", getStackTrace(ex));
|
||||||
|
|
||||||
// 添加系统信息
|
|
||||||
errorData.put("osName", System.getProperty("os.name"));
|
errorData.put("osName", System.getProperty("os.name"));
|
||||||
errorData.put("osVersion", System.getProperty("os.version"));
|
errorData.put("osVersion", System.getProperty("os.version"));
|
||||||
errorData.put("appVersion", System.getProperty("project.version", "unknown"));
|
errorData.put("appVersion", System.getProperty("project.version", "unknown"));
|
||||||
|
|
||||||
String url = serverUrl + "/monitor/client/api/error";
|
apiForwarder.post("/monitor/error", errorData, null);
|
||||||
restTemplate.postForObject(url, errorData, Map.class);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("错误上报失败: " + e.getMessage());
|
// 静默失败,不影响主业务
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,10 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Excel 解析工具类
|
* Excel 解析工具类
|
||||||
* 统一处理各种平台的 Excel 文件解析需求
|
|
||||||
*
|
|
||||||
* @author Claude Code
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ExcelParseUtil {
|
public class ExcelParseUtil {
|
||||||
|
|
||||||
/**
|
|
||||||
* 自动查找表头行索引(在前2行中查找)
|
|
||||||
* @param rows Excel所有行
|
|
||||||
* @param columnName 列名(如"品牌")
|
|
||||||
* @return 表头行索引,未找到返回-1
|
|
||||||
*/
|
|
||||||
private static int findHeaderRow(List<List<Object>> rows, String columnName) {
|
private static int findHeaderRow(List<List<Object>> rows, String columnName) {
|
||||||
for (int r = 0; r < Math.min(2, rows.size()); r++) {
|
for (int r = 0; r < Math.min(2, rows.size()); r++) {
|
||||||
for (Object cell : rows.get(r)) {
|
for (Object cell : rows.get(r)) {
|
||||||
@@ -34,93 +25,38 @@ public class ExcelParseUtil {
|
|||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static int findColumnIndex(List<Object> headerRow, String columnName) {
|
||||||
* 解析 Excel 文件第一列数据
|
for (int c = 0; c < headerRow.size(); c++) {
|
||||||
* 通用方法,适用于店铺名、ASIN、订单号等标识符解析
|
if (headerRow.get(c) != null && columnName.equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
|
||||||
*
|
return c;
|
||||||
* @param file Excel 文件
|
|
||||||
* @return 解析出的字符串列表,跳过表头,过滤空值
|
|
||||||
*/
|
|
||||||
public static List<String> parseFirstColumn(MultipartFile file) {
|
|
||||||
List<String> result = new ArrayList<>();
|
|
||||||
if (file == null || file.isEmpty()) {
|
|
||||||
log.warn("Excel 文件为空");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream in = file.getInputStream()) {
|
|
||||||
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
|
||||||
List<List<Object>> rows = reader.read();
|
|
||||||
|
|
||||||
for (int i = 1; i < rows.size(); i++) { // 从第2行开始,跳过表头
|
|
||||||
List<Object> row = rows.get(i);
|
|
||||||
if (row != null && !row.isEmpty()) {
|
|
||||||
Object cell = row.get(0); // 获取第一列
|
|
||||||
if (cell != null) {
|
|
||||||
String value = cell.toString().trim();
|
|
||||||
if (!value.isEmpty()) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("成功解析 Excel 文件: {}, 共解析出 {} 条数据",
|
|
||||||
file.getOriginalFilename(), result.size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("解析 Excel 文件失败: {}, 文件名: {}", e.getMessage(),
|
|
||||||
file.getOriginalFilename(), e);
|
|
||||||
}
|
}
|
||||||
return result;
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> parseFirstColumn(MultipartFile file) {
|
||||||
|
return parseColumn(file, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析指定列的数据
|
|
||||||
*
|
|
||||||
* @param file Excel 文件
|
|
||||||
* @param columnIndex 列索引(从0开始)
|
|
||||||
* @return 解析出的字符串列表
|
|
||||||
*/
|
|
||||||
public static List<String> parseColumn(MultipartFile file, int columnIndex) {
|
public static List<String> parseColumn(MultipartFile file, int columnIndex) {
|
||||||
List<String> result = new ArrayList<>();
|
List<String> result = new ArrayList<>();
|
||||||
if (file == null || file.isEmpty()) {
|
|
||||||
log.warn("Excel 文件为空");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream in = file.getInputStream()) {
|
try (InputStream in = file.getInputStream()) {
|
||||||
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
||||||
List<List<Object>> rows = reader.read();
|
List<List<Object>> rows = reader.read();
|
||||||
|
for (int i = 1; i < rows.size(); i++) {
|
||||||
for (int i = 1; i < rows.size(); i++) { // 从第2行开始,跳过表头
|
|
||||||
List<Object> row = rows.get(i);
|
List<Object> row = rows.get(i);
|
||||||
if (row != null && row.size() > columnIndex) {
|
if (row.size() > columnIndex && row.get(columnIndex) != null) {
|
||||||
Object cell = row.get(columnIndex);
|
String value = row.get(columnIndex).toString().trim();
|
||||||
if (cell != null) {
|
if (!value.isEmpty()) result.add(value);
|
||||||
String value = cell.toString().trim();
|
|
||||||
if (!value.isEmpty()) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("成功解析 Excel 文件第{}列: {}, 共解析出 {} 条数据",
|
|
||||||
columnIndex + 1, file.getOriginalFilename(), result.size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解析 Excel 文件第{}列失败: {}, 文件名: {}",
|
log.error("解析Excel失败", e);
|
||||||
columnIndex + 1, e.getMessage(), file.getOriginalFilename(), e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据列名解析数据(自动适配第1行或第2行为表头)
|
|
||||||
*/
|
|
||||||
public static List<String> parseColumnByName(MultipartFile file, String columnName) {
|
public static List<String> parseColumnByName(MultipartFile file, String columnName) {
|
||||||
List<String> result = new ArrayList<>();
|
List<String> result = new ArrayList<>();
|
||||||
try (InputStream in = file.getInputStream()) {
|
try (InputStream in = file.getInputStream()) {
|
||||||
@@ -131,14 +67,8 @@ public class ExcelParseUtil {
|
|||||||
int headerRow = findHeaderRow(rows, columnName);
|
int headerRow = findHeaderRow(rows, columnName);
|
||||||
if (headerRow < 0) return result;
|
if (headerRow < 0) return result;
|
||||||
|
|
||||||
int colIdx = -1;
|
int colIdx = findColumnIndex(rows.get(headerRow), columnName);
|
||||||
for (int c = 0; c < rows.get(headerRow).size(); c++) {
|
if (colIdx < 0) return result;
|
||||||
if (rows.get(headerRow).get(c) != null &&
|
|
||||||
columnName.equals(rows.get(headerRow).get(c).toString().replaceAll("\\s+", ""))) {
|
|
||||||
colIdx = c;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = headerRow + 1; i < rows.size(); i++) {
|
for (int i = headerRow + 1; i < rows.size(); i++) {
|
||||||
List<Object> row = rows.get(i);
|
List<Object> row = rows.get(i);
|
||||||
@@ -153,11 +83,6 @@ public class ExcelParseUtil {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取Excel的完整数据(包含表头和所有行,自动适配第1行或第2行为表头)
|
|
||||||
* @param file Excel文件
|
|
||||||
* @return Map包含headers(表头列表)和rows(数据行列表,每行是Map)
|
|
||||||
*/
|
|
||||||
public static Map<String, Object> parseFullExcel(MultipartFile file) {
|
public static Map<String, Object> parseFullExcel(MultipartFile file) {
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
List<String> headers = new ArrayList<>();
|
List<String> headers = new ArrayList<>();
|
||||||
@@ -166,17 +91,13 @@ public class ExcelParseUtil {
|
|||||||
try (InputStream in = file.getInputStream()) {
|
try (InputStream in = file.getInputStream()) {
|
||||||
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
||||||
List<List<Object>> allRows = reader.read();
|
List<List<Object>> allRows = reader.read();
|
||||||
|
|
||||||
if (allRows.isEmpty()) {
|
if (allRows.isEmpty()) {
|
||||||
log.warn("Excel文件为空");
|
|
||||||
result.put("headers", headers);
|
result.put("headers", headers);
|
||||||
result.put("rows", rows);
|
result.put("rows", rows);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int headerRowIndex = Math.max(0, findHeaderRow(allRows, "品牌"));
|
int headerRowIndex = Math.max(0, findHeaderRow(allRows, "品牌"));
|
||||||
log.info("检测到表头行:第{}行", headerRowIndex + 1);
|
|
||||||
|
|
||||||
for (Object cell : allRows.get(headerRowIndex)) {
|
for (Object cell : allRows.get(headerRowIndex)) {
|
||||||
headers.add(cell != null ? cell.toString().trim() : "");
|
headers.add(cell != null ? cell.toString().trim() : "");
|
||||||
}
|
}
|
||||||
@@ -192,89 +113,21 @@ public class ExcelParseUtil {
|
|||||||
|
|
||||||
result.put("headers", headers);
|
result.put("headers", headers);
|
||||||
result.put("rows", rows);
|
result.put("rows", rows);
|
||||||
log.info("解析Excel: {}, 表头{}列, 数据{}行", file.getOriginalFilename(), headers.size(), rows.size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解析Excel失败: {}", e.getMessage(), e);
|
log.error("解析Excel失败", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ASIN列表从Excel中过滤完整行数据(自动适配第1行或第2行为表头)
|
|
||||||
* @param file Excel文件
|
|
||||||
* @param asins ASIN列表
|
|
||||||
* @return Map包含headers(表头)和filteredRows(过滤后的完整行数据)
|
|
||||||
*/
|
|
||||||
public static Map<String, Object> filterExcelByAsins(MultipartFile file, List<String> asins) {
|
public static Map<String, Object> filterExcelByAsins(MultipartFile file, List<String> asins) {
|
||||||
Map<String, Object> result = new HashMap<>();
|
return filterExcelByColumn(file, "ASIN", asins);
|
||||||
List<String> headers = new ArrayList<>();
|
|
||||||
List<Map<String, Object>> filteredRows = new ArrayList<>();
|
|
||||||
|
|
||||||
try (InputStream in = file.getInputStream()) {
|
|
||||||
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
|
||||||
List<List<Object>> allRows = reader.read();
|
|
||||||
|
|
||||||
if (allRows.isEmpty()) {
|
|
||||||
result.put("headers", headers);
|
|
||||||
result.put("filteredRows", filteredRows);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int headerRowIndex = Math.max(0, findHeaderRow(allRows, "ASIN"));
|
|
||||||
if (headerRowIndex < 0) {
|
|
||||||
log.warn("未找到'ASIN'列");
|
|
||||||
result.put("headers", headers);
|
|
||||||
result.put("filteredRows", filteredRows);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int asinColIndex = -1;
|
|
||||||
List<Object> headerRow = allRows.get(headerRowIndex);
|
|
||||||
for (int c = 0; c < headerRow.size(); c++) {
|
|
||||||
if (headerRow.get(c) != null && "ASIN".equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
|
|
||||||
asinColIndex = c;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Object cell : headerRow) {
|
|
||||||
headers.add(cell != null ? cell.toString().trim() : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> asinSet = asins.stream().map(String::trim).collect(Collectors.toSet());
|
|
||||||
|
|
||||||
for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
|
|
||||||
List<Object> row = allRows.get(i);
|
|
||||||
if (row.size() > asinColIndex && row.get(asinColIndex) != null
|
|
||||||
&& asinSet.contains(row.get(asinColIndex).toString().trim())) {
|
|
||||||
Map<String, Object> rowMap = new HashMap<>();
|
|
||||||
for (int j = 0; j < Math.min(headers.size(), row.size()); j++) {
|
|
||||||
rowMap.put(headers.get(j), row.get(j));
|
|
||||||
}
|
|
||||||
filteredRows.add(rowMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.put("headers", headers);
|
|
||||||
result.put("filteredRows", filteredRows);
|
|
||||||
log.info("ASIN过滤: {}, {}个ASIN -> {}行数据", file.getOriginalFilename(), asins.size(), filteredRows.size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("ASIN过滤失败: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据品牌列表从Excel中过滤完整行数据(自动适配第1行或第2行为表头)
|
|
||||||
* @param file Excel文件
|
|
||||||
* @param brands 品牌列表
|
|
||||||
* @return Map包含headers(表头)和filteredRows(过滤后的完整行数据)
|
|
||||||
*/
|
|
||||||
public static Map<String, Object> filterExcelByBrands(MultipartFile file, List<String> brands) {
|
public static Map<String, Object> filterExcelByBrands(MultipartFile file, List<String> brands) {
|
||||||
|
return filterExcelByColumn(file, "品牌", brands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> filterExcelByColumn(MultipartFile file, String columnName, List<String> values) {
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
List<String> headers = new ArrayList<>();
|
List<String> headers = new ArrayList<>();
|
||||||
List<Map<String, Object>> filteredRows = new ArrayList<>();
|
List<Map<String, Object>> filteredRows = new ArrayList<>();
|
||||||
@@ -282,40 +135,37 @@ public class ExcelParseUtil {
|
|||||||
try (InputStream in = file.getInputStream()) {
|
try (InputStream in = file.getInputStream()) {
|
||||||
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
||||||
List<List<Object>> allRows = reader.read();
|
List<List<Object>> allRows = reader.read();
|
||||||
|
|
||||||
if (allRows.isEmpty()) {
|
if (allRows.isEmpty()) {
|
||||||
result.put("headers", headers);
|
result.put("headers", headers);
|
||||||
result.put("filteredRows", filteredRows);
|
result.put("filteredRows", filteredRows);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int headerRowIndex = findHeaderRow(allRows, "品牌");
|
int headerRowIndex = findHeaderRow(allRows, columnName);
|
||||||
if (headerRowIndex < 0) {
|
if (headerRowIndex < 0) {
|
||||||
log.warn("未找到'品牌'列");
|
|
||||||
result.put("headers", headers);
|
result.put("headers", headers);
|
||||||
result.put("filteredRows", filteredRows);
|
result.put("filteredRows", filteredRows);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int brandColIndex = -1;
|
|
||||||
List<Object> headerRow = allRows.get(headerRowIndex);
|
List<Object> headerRow = allRows.get(headerRowIndex);
|
||||||
for (int c = 0; c < headerRow.size(); c++) {
|
int colIndex = findColumnIndex(headerRow, columnName);
|
||||||
if (headerRow.get(c) != null && "品牌".equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
|
if (colIndex < 0) {
|
||||||
brandColIndex = c;
|
result.put("headers", headers);
|
||||||
break;
|
result.put("filteredRows", filteredRows);
|
||||||
}
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Object cell : headerRow) {
|
for (Object cell : headerRow) {
|
||||||
headers.add(cell != null ? cell.toString().trim() : "");
|
headers.add(cell != null ? cell.toString().trim() : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> brandSet = brands.stream().map(String::trim).collect(Collectors.toSet());
|
Set<String> valueSet = values.stream().map(String::trim).collect(Collectors.toSet());
|
||||||
|
|
||||||
for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
|
for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
|
||||||
List<Object> row = allRows.get(i);
|
List<Object> row = allRows.get(i);
|
||||||
if (row.size() > brandColIndex && row.get(brandColIndex) != null
|
if (row.size() > colIndex && row.get(colIndex) != null
|
||||||
&& brandSet.contains(row.get(brandColIndex).toString().trim())) {
|
&& valueSet.contains(row.get(colIndex).toString().trim())) {
|
||||||
Map<String, Object> rowMap = new HashMap<>();
|
Map<String, Object> rowMap = new HashMap<>();
|
||||||
for (int j = 0; j < Math.min(headers.size(), row.size()); j++) {
|
for (int j = 0; j < Math.min(headers.size(), row.size()); j++) {
|
||||||
rowMap.put(headers.get(j), row.get(j));
|
rowMap.put(headers.get(j), row.get(j));
|
||||||
@@ -326,12 +176,9 @@ public class ExcelParseUtil {
|
|||||||
|
|
||||||
result.put("headers", headers);
|
result.put("headers", headers);
|
||||||
result.put("filteredRows", filteredRows);
|
result.put("filteredRows", filteredRows);
|
||||||
log.info("品牌过滤: {}, {}个品牌 -> {}行数据", file.getOriginalFilename(), brands.size(), filteredRows.size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("品牌过滤失败: {}", e.getMessage(), e);
|
log.error("Excel过滤失败", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,30 +2,48 @@ package com.tashow.erp.utils;
|
|||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代理IP池
|
* 代理IP池
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class ProxyPool {
|
public class ProxyPool {
|
||||||
|
private static final String API_URL = "http://api.tianqiip.com/getip?secret=y0thbcco1rgxn9e9&num=%d&type=txt&port=2&time=3&mr=1&sign=9be780c7e27aea815f1e0874446b9e35";
|
||||||
private static final String API_URL = "http://api.tianqiip.com/getip?secret=h6x09x0eenxuf4s7&num=1&type=txt&port=2&time=3&mr=1&sign=620719f6b7d66744b0216a4f61a6bcee";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一个代理IP
|
* 获取一个代理IP
|
||||||
* @return 代理地址,格式:host:port,如 123.96.236.32:40016
|
* @return 代理地址,格式:host:port,如 123.96.236.32:40016
|
||||||
*/
|
*/
|
||||||
public String getProxy() {
|
public String getProxy() {
|
||||||
|
List<String> proxies = getProxies(1);
|
||||||
|
return proxies.isEmpty() ? null : proxies.get(0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 批量获取代理IP
|
||||||
|
* @param num 需要获取的代理数量
|
||||||
|
* @return 代理地址列表
|
||||||
|
*/
|
||||||
|
public List<String> getProxies(int num) {
|
||||||
|
List<String> proxies = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
String response = HttpUtil.get(API_URL);
|
String url = String.format(API_URL, num);
|
||||||
|
String response = HttpUtil.get(url);
|
||||||
if (response != null && !response.trim().isEmpty()) {
|
if (response != null && !response.trim().isEmpty()) {
|
||||||
String proxy = response.trim();
|
String[] lines = response.trim().split("\n");
|
||||||
System.out.println("获取到代理: " + proxy);
|
for (String line : lines) {
|
||||||
return proxy;
|
String proxy = line.trim();
|
||||||
|
if (!proxy.isEmpty()) {
|
||||||
|
proxies.add(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("获取到 " + proxies.size() + " 个代理");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("获取代理失败: " + e.getMessage());
|
System.err.println("获取代理失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
return null;
|
return proxies;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ public class QiniuUtil {
|
|||||||
// 七牛配置
|
// 七牛配置
|
||||||
private static final String ACCESS_KEY = "M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF";
|
private static final String ACCESS_KEY = "M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF";
|
||||||
private static final String SECRET_KEY = "Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl";
|
private static final String SECRET_KEY = "Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl";
|
||||||
private static final String BUCKET = "pxdj-prod";
|
private static final String BUCKET = "bydj-prod";
|
||||||
private static final String DOMAIN = "https://qiniu.pxdj.tashowz.com/"; // 开启HTTPS的外链域名
|
private static final String DOMAIN = "https://qiniu.bydj.tashowz.com/"; // 开启HTTPS的外链域名
|
||||||
|
|
||||||
public static String uploadFromUrl(String imageUrl, String key) throws Exception {
|
public static String uploadFromUrl(String imageUrl, String key) throws Exception {
|
||||||
// 1. 下载远程图片
|
// 1. 下载远程图片
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.tashow.erp.utils;
|
package com.tashow.erp.utils;
|
||||||
|
|
||||||
|
import com.tashow.erp.common.HttpConstants;
|
||||||
import com.tashow.erp.common.Constants;
|
|
||||||
import org.springframework.util.AntPathMatcher;
|
import org.springframework.util.AntPathMatcher;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -362,7 +361,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
|
|||||||
*/
|
*/
|
||||||
public static boolean ishttp(String link)
|
public static boolean ishttp(String link)
|
||||||
{
|
{
|
||||||
return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS);
|
return StringUtils.startsWithAny(link, HttpConstants.HTTP, HttpConstants.HTTPS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -635,9 +634,10 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
AntPathMatcher matcher = new AntPathMatcher();
|
||||||
for (String pattern : strs)
|
for (String pattern : strs)
|
||||||
{
|
{
|
||||||
if (isMatch(pattern, str))
|
if (matcher.match(pattern, str))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -645,28 +645,6 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断url是否与规则配置:
|
|
||||||
* ? 表示单个字符;
|
|
||||||
* * 表示一层路径内的任意字符串,不可跨层级;
|
|
||||||
* ** 表示任意层路径;
|
|
||||||
*
|
|
||||||
* @param pattern 匹配规则
|
|
||||||
* @param url 需要匹配的url
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static boolean isMatch(String pattern, String url)
|
|
||||||
{
|
|
||||||
AntPathMatcher matcher = new AntPathMatcher();
|
|
||||||
return matcher.match(pattern, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T> T cast(Object obj)
|
|
||||||
{
|
|
||||||
return (T) obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
|
* 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,156 +1,217 @@
|
|||||||
package com.tashow.erp.utils;
|
package com.tashow.erp.utils;
|
||||||
import com.tashow.erp.service.BrandTrademarkCacheService;
|
|
||||||
import jakarta.annotation.PreDestroy;
|
|
||||||
import org.openqa.selenium.JavascriptExecutor;
|
import org.openqa.selenium.JavascriptExecutor;
|
||||||
import org.openqa.selenium.chrome.ChromeDriver;
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 商标检查工具
|
* 商标检查工具 - 无状态设计
|
||||||
* 检测到403时自动切换代理并重试
|
* 每次调用使用独立的Driver和代理
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class TrademarkCheckUtil {
|
public class TrademarkCheckUtil {
|
||||||
@Autowired
|
|
||||||
private ProxyPool proxyPool;
|
private static String normalize(String name) {
|
||||||
@Autowired
|
return name.toLowerCase().replaceAll("[^a-z0-9]", "");
|
||||||
private BrandTrademarkCacheService cacheService;
|
|
||||||
private ChromeDriver driver;
|
|
||||||
|
|
||||||
private synchronized void ensureInit() {
|
|
||||||
if (driver == null) {
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
try {
|
|
||||||
driver = SeleniumUtil.createDriver(true, proxyPool.getProxy());
|
|
||||||
driver.get("https://tmsearch.uspto.gov/search/search-results");
|
|
||||||
Thread.sleep(6000);
|
|
||||||
return; // 成功则返回
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("初始化失败(尝试" + (i+1) + "/3): " + e.getMessage());
|
|
||||||
if (driver != null) {
|
|
||||||
try { driver.quit(); } catch (Exception ex) {}
|
|
||||||
driver = null;
|
|
||||||
}
|
|
||||||
if (i == 2) throw new RuntimeException("初始化失败,已重试3次", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) {
|
/**
|
||||||
ensureInit();
|
* 批量检查商标(使用指定代理)
|
||||||
|
* @param brands 品牌列表
|
||||||
|
* @param proxy 代理地址,格式:host:port
|
||||||
|
* @return 检查结果 Map<品牌, 是否已注册>
|
||||||
|
*/
|
||||||
|
public static Map<String, Boolean> batchCheck(List<String> brands, String proxy) {
|
||||||
|
return batchCheck(brands, proxy, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量检查商标(使用指定代理,支持取消检查)
|
||||||
|
* @param brands 品牌列表
|
||||||
|
* @param proxy 代理地址,格式:host:port
|
||||||
|
* @param taskId 任务ID,用于取消检查
|
||||||
|
* @param cancelMap 取消状态映射
|
||||||
|
* @return 检查结果 Map<品牌, 是否已注册>
|
||||||
|
*/
|
||||||
|
public static Map<String, Boolean> batchCheck(List<String> brands, String proxy, String taskId, java.util.Map<String, Boolean> cancelMap) {
|
||||||
|
return batchCheck(brands, proxy, taskId, cancelMap, -1, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, Boolean> batchCheck(List<String> brands, String proxy, String taskId, java.util.Map<String, Boolean> cancelMap, int processIndex) {
|
||||||
|
return batchCheck(brands, proxy, taskId, cancelMap, processIndex, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量检查商标(使用指定代理,支持取消检查和进程标识)
|
||||||
|
* @param brands 品牌列表
|
||||||
|
* @param proxy 代理地址,格式:host:port
|
||||||
|
* @param taskId 任务ID,用于取消检查
|
||||||
|
* @param cancelMap 取消状态映射
|
||||||
|
* @param processIndex 进程索引,用于日志标识
|
||||||
|
* @return 检查结果 Map<品牌, 是否已注册>
|
||||||
|
*/
|
||||||
|
public static Map<String, Boolean> batchCheck(List<String> brands, String proxy, String taskId, java.util.Map<String, Boolean> cancelMap, int processIndex, java.util.Map<String, org.springframework.web.servlet.mvc.method.annotation.SseEmitter> sseEmitters) {
|
||||||
|
Map<String, Boolean> resultMap = new HashMap<>();
|
||||||
|
String processPrefix = processIndex >= 0 ? "进程" + processIndex + ":" : "";
|
||||||
|
|
||||||
// 构建批量查询脚本(带错误诊断)
|
ChromeDriver driver = null;
|
||||||
String script = """
|
String currentProxy = proxy; // 当前使用的代理
|
||||||
const brands = arguments[0];
|
|
||||||
const callback = arguments[arguments.length - 1];
|
// 初始化Driver,失败时重试并换IP
|
||||||
|
int initRetryCount = 0;
|
||||||
Promise.all(brands.map(brand =>
|
while (initRetryCount < 5 && driver == null) {
|
||||||
fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', {
|
try {
|
||||||
method: 'POST',
|
driver = SeleniumUtil.createDriver(true, currentProxy);
|
||||||
headers: {'Content-Type': 'application/json'},
|
driver.get("https://tmsearch.uspto.gov/search/search-results");
|
||||||
body: JSON.stringify({
|
Thread.sleep(6000);
|
||||||
query: {bool: {must: [{bool: {should: [
|
break; // 成功则跳出循环
|
||||||
{match_phrase: {WM: {query: brand, boost: 5}}},
|
} catch (Exception initError) {
|
||||||
{match: {WM: {query: brand, boost: 2}}},
|
System.err.println(processPrefix + "Driver初始化失败(" + (initRetryCount + 1) + "/5): " + initError.getMessage());
|
||||||
{match_phrase: {PM: {query: brand, boost: 2}}}
|
|
||||||
]}}]}},
|
if (driver != null) {
|
||||||
size: 1, _source: ['alive']
|
try { driver.quit(); } catch (Exception ignored) {}
|
||||||
})
|
driver = null;
|
||||||
})
|
}
|
||||||
.then(r => {
|
|
||||||
if (!r.ok) {
|
// 获取新代理重试
|
||||||
return {brand, alive: false, error: `HTTP ${r.status}: ${r.statusText}`};
|
if (initRetryCount < 2) {
|
||||||
|
try {
|
||||||
|
ProxyPool proxyPool = new ProxyPool();
|
||||||
|
String newProxy = proxyPool.getProxy();
|
||||||
|
if (newProxy != null) {
|
||||||
|
currentProxy = newProxy;
|
||||||
|
System.out.println(processPrefix + "初始化失败,切换到新代理: " + currentProxy);
|
||||||
|
} else {
|
||||||
|
System.err.println(processPrefix + "获取新代理失败,使用原代理重试");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println(processPrefix + "获取新代理异常: " + e.getMessage());
|
||||||
}
|
}
|
||||||
return r.json().then(d => ({
|
}
|
||||||
brand,
|
|
||||||
alive: d?.hits?.hits?.[0]?.source?.alive || false,
|
initRetryCount++;
|
||||||
error: null
|
if (initRetryCount < 3) {
|
||||||
}));
|
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
|
||||||
})
|
}
|
||||||
.catch(e => ({
|
}
|
||||||
brand,
|
}
|
||||||
alive: false,
|
|
||||||
error: e.name + ': ' + e.message
|
if (driver == null) {
|
||||||
}))
|
System.err.println(processPrefix + "Driver初始化失败,已重试3次,跳过该批次");
|
||||||
)).then(callback);
|
return resultMap;
|
||||||
""";
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Map<String, Object>> results = (List<Map<String, Object>>)
|
|
||||||
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
|
|
||||||
|
|
||||||
// 检测是否有网络错误(包括403、Failed to fetch等)
|
for (String brand : brands) {
|
||||||
boolean hasNetworkError = results.stream()
|
// 检查是否已取消
|
||||||
.anyMatch(item -> {
|
if (taskId != null && cancelMap != null && cancelMap.getOrDefault(taskId, false)) {
|
||||||
String error = (String) item.get("error");
|
System.out.println("检测到任务已取消,停止处理品牌: " + brand);
|
||||||
return error != null && (
|
break;
|
||||||
error.contains("HTTP 403") ||
|
}
|
||||||
error.contains("Failed to fetch") ||
|
|
||||||
error.contains("NetworkError") ||
|
|
||||||
error.contains("TypeError")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果有网络错误,切换代理并重试
|
|
||||||
if (hasNetworkError) {
|
|
||||||
System.err.println("检测到网络错误,切换代理并重试...");
|
|
||||||
|
|
||||||
// 切换代理前保存已查询的品牌
|
int retryCount = 0;
|
||||||
if (alreadyQueried != null && !alreadyQueried.isEmpty()) {
|
boolean success = false;
|
||||||
|
|
||||||
|
while (retryCount < 5 && !success) {
|
||||||
|
// 在重试循环中也检查取消状态
|
||||||
|
if (taskId != null && cancelMap != null && cancelMap.getOrDefault(taskId, false)) {
|
||||||
|
System.out.println("检测到任务已取消,停止重试品牌: " + brand);
|
||||||
|
return resultMap;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
cacheService.saveResults(alreadyQueried);
|
String script = "fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:{bool:{must:[{bool:{should:[{match_phrase:{WM:{query:'" + brand.replace("'", "\\'")+"',boost:5}}}]}}]}},size:100})}).then(r=>{if(!r.ok){return arguments[arguments.length-1]({hits:[],error:'HTTP '+r.status+': '+r.statusText});}return r.text().then(text=>{if(text.startsWith('<!DOCTYPE')||text.startsWith('<html')){return arguments[arguments.length-1]({hits:[],error:'HTML response detected, likely blocked'});}try{const d=JSON.parse(text);return arguments[arguments.length-1]({hits:d?.hits?.hits||[],error:null});}catch(e){return arguments[arguments.length-1]({hits:[],error:'JSON parse error: '+e.message});}});}).catch(e=>arguments[arguments.length-1]({hits:[],error:e.message}));";
|
||||||
System.out.println("代理切换,已保存 " + alreadyQueried.size() + " 个品牌到缓存");
|
@SuppressWarnings("unchecked") Map<String, Object> result = (Map<String, Object>) ((JavascriptExecutor) driver).executeAsyncScript(script);
|
||||||
|
String error = (String) result.get("error");
|
||||||
|
|
||||||
|
if (error != null && (error.contains("HTTP 403") || error.contains("Failed to fetch") || error.contains("NetworkError") || error.contains("TypeError") || error.contains("script timeout"))) {
|
||||||
|
System.err.println(processPrefix + brand + " 查询失败(" + (retryCount + 1) + "/5): " + error + ",切换代理...");
|
||||||
|
|
||||||
|
// 对于网络错误,获取新代理并重新创建Driver
|
||||||
|
if (error.contains("Failed to fetch") || error.contains("HTTP 403") || error.contains("NetworkError") || error.contains("ERR_CONNECTION_RESET")) {
|
||||||
|
try {
|
||||||
|
if (driver != null) driver.quit();
|
||||||
|
|
||||||
|
// 获取新的代理IP
|
||||||
|
ProxyPool proxyPool = new ProxyPool();
|
||||||
|
String newProxy = proxyPool.getProxy();
|
||||||
|
if (newProxy != null) {
|
||||||
|
currentProxy = newProxy;
|
||||||
|
System.out.println(processPrefix + "切换到新代理: " + currentProxy);
|
||||||
|
} else {
|
||||||
|
System.err.println(processPrefix + "获取新代理失败,使用原代理");
|
||||||
|
}
|
||||||
|
|
||||||
|
driver = SeleniumUtil.createDriver(true, currentProxy);
|
||||||
|
driver.get("https://tmsearch.uspto.gov/search/search-results");
|
||||||
|
Thread.sleep(3000); // 缩短等待时间
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println(processPrefix + "重新创建Driver失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error == null) {
|
||||||
|
@SuppressWarnings("unchecked") List<Map<String, Object>> hits = (List<Map<String, Object>>) result.get("hits");
|
||||||
|
String input = normalize(brand);
|
||||||
|
boolean registered = false;
|
||||||
|
|
||||||
|
for (Map<String, Object> hit : hits) {
|
||||||
|
@SuppressWarnings("unchecked") Map<String, Object> source = (Map<String, Object>) hit.get("source");
|
||||||
|
if (source != null && input.equals(normalize((String) source.get("wordmark")))) {
|
||||||
|
Number code = (Number) source.get("statusCode");
|
||||||
|
if (code != null && (code.intValue() == 688 || code.intValue() == 700 || code.intValue() == 686)) {
|
||||||
|
registered = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultMap.put(brand, registered);
|
||||||
|
System.out.println(processPrefix + brand + " -> " + (registered ? "✓" : "✗"));
|
||||||
|
success = true;
|
||||||
|
|
||||||
|
// 推送SSE进度
|
||||||
|
if (taskId != null && sseEmitters != null) {
|
||||||
|
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = sseEmitters.get(taskId);
|
||||||
|
if (emitter != null) {
|
||||||
|
try {
|
||||||
|
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event()
|
||||||
|
.name("progress")
|
||||||
|
.data(processPrefix + brand + " -> " + (registered ? "✓" : "✗")));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.err.println(processPrefix + brand + " -> [查询失败: " + error + "]");
|
||||||
|
resultMap.put(brand, true);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("保存缓存失败: " + e.getMessage());
|
System.err.println(processPrefix + brand + " 查询异常(" + (retryCount + 1) + "/5): " + e.getMessage());
|
||||||
|
retryCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try { driver.quit(); } catch (Exception e) {}
|
if (!success) {
|
||||||
driver = null;
|
System.err.println(processPrefix + brand + " -> [查询失败: 已重试5次]");
|
||||||
ensureInit();
|
resultMap.put(brand, true);
|
||||||
|
|
||||||
// 重新执行查询
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Map<String, Object>> retryResults = (List<Map<String, Object>>)
|
|
||||||
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
|
|
||||||
results = retryResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Boolean> resultMap = new HashMap<>();
|
|
||||||
for (Map<String, Object> item : results) {
|
|
||||||
String brand = (String) item.get("brand");
|
|
||||||
Boolean alive = (Boolean) item.get("alive");
|
|
||||||
String error = (String) item.get("error");
|
|
||||||
|
|
||||||
if (error != null) {
|
|
||||||
// 查询失败,不放入结果,只打印错误
|
|
||||||
System.err.println(brand + " -> [查询失败: " + error + "]");
|
|
||||||
} else {
|
|
||||||
// 查询成功,放入结果
|
|
||||||
resultMap.put(brand, alive);
|
|
||||||
System.out.println(brand + " -> " + (alive ? "✓ 已注册" : "✗ 未注册"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resultMap;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("批量查询失败: " + e.getMessage());
|
System.err.println("Driver初始化失败: " + e.getMessage());
|
||||||
return new HashMap<>();
|
} finally {
|
||||||
|
if (driver != null) {
|
||||||
|
try {
|
||||||
|
driver.quit();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return resultMap;
|
||||||
public synchronized void closeDriver() {
|
|
||||||
if (driver != null) {
|
|
||||||
try { driver.quit(); } catch (Exception e) {}
|
|
||||||
driver = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreDestroy
|
|
||||||
public void cleanup() {
|
|
||||||
closeDriver();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.tashow.erp.utils;
|
||||||
|
|
||||||
|
import com.tashow.erp.common.AmazonConstants;
|
||||||
|
import com.tashow.erp.common.RakutenConstants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL构建工具类
|
||||||
|
*/
|
||||||
|
public class UrlBuilder {
|
||||||
|
/**
|
||||||
|
* 构建亚马逊商品URL
|
||||||
|
*/
|
||||||
|
public static String buildAmazonUrl(String region, String asin) {
|
||||||
|
String domain = AmazonConstants.REGION_US.equals(region)
|
||||||
|
? AmazonConstants.DOMAIN_US
|
||||||
|
: AmazonConstants.DOMAIN_JP;
|
||||||
|
return domain + AmazonConstants.URL_PRODUCT_PATH + asin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建乐天商品URL
|
||||||
|
*/
|
||||||
|
public static String buildRakutenUrl(String itemCode) {
|
||||||
|
return RakutenConstants.DOMAIN + "/" + itemCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UrlBuilder() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.tashow.erp.utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据验证工具类
|
||||||
|
*/
|
||||||
|
public class ValidationUtils {
|
||||||
|
/**
|
||||||
|
* 判断字符串是否为空
|
||||||
|
*/
|
||||||
|
public static boolean isEmpty(String str) {
|
||||||
|
return str == null || str.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断字符串是否非空
|
||||||
|
*/
|
||||||
|
public static boolean isNotEmpty(String str) {
|
||||||
|
return !isEmpty(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationUtils() {}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ javafx:
|
|||||||
height: 800
|
height: 800
|
||||||
# style: DECORATED # javafx.stage.StageStyle [DECORATED, UNDECORATED, TRANSPARENT, UTILITY, UNIFIED]
|
# style: DECORATED # javafx.stage.StageStyle [DECORATED, UNDECORATED, TRANSPARENT, UTILITY, UNIFIED]
|
||||||
# resizable: false
|
# resizable: false
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
main:
|
main:
|
||||||
lazy-initialization: true
|
lazy-initialization: true
|
||||||
@@ -47,8 +46,9 @@ server:
|
|||||||
api:
|
api:
|
||||||
server:
|
server:
|
||||||
# 主服务器API配置
|
# 主服务器API配置
|
||||||
#base-url: "http://8.138.23.49:8085"
|
base-url: "http://8.138.23.49:8085"
|
||||||
base-url: "http://192.168.1.89:8085"
|
#base-url: "http://192.168.1.89:8085"
|
||||||
|
#base-url: "http://127.0.0.1:8085"
|
||||||
paths:
|
paths:
|
||||||
monitor: "/monitor/client/api"
|
monitor: "/monitor/client/api"
|
||||||
login: "/monitor/account/login"
|
login: "/monitor/account/login"
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ import cn.hutool.core.date.DateUtil;
|
|||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 客户端账号控制器
|
* 客户端账号控制器
|
||||||
*
|
*
|
||||||
@@ -76,6 +73,9 @@ public class ClientAccountController extends BaseController {
|
|||||||
private Auth auth;
|
private Auth auth;
|
||||||
|
|
||||||
private static final String SPLASH_IMAGE_CACHE_KEY = "splash_image:";
|
private static final String SPLASH_IMAGE_CACHE_KEY = "splash_image:";
|
||||||
|
private static final String BRAND_LOGO_CACHE_KEY = "brand_logo:";
|
||||||
|
private static final String GLOBAL_SPLASH_IMAGE_KEY = "global_splash_image";
|
||||||
|
private static final String GLOBAL_BRAND_LOGO_KEY = "global_brand_logo";
|
||||||
|
|
||||||
private AjaxResult checkDeviceLimit(Long accountId, String deviceId, int deviceLimit) {
|
private AjaxResult checkDeviceLimit(Long accountId, String deviceId, int deviceLimit) {
|
||||||
int activeDeviceCount = accountDeviceMapper.countActiveDevicesByAccountId(accountId);
|
int activeDeviceCount = accountDeviceMapper.countActiveDevicesByAccountId(accountId);
|
||||||
@@ -180,6 +180,11 @@ public class ClientAccountController extends BaseController {
|
|||||||
if (StringUtils.isNotEmpty(account.getSplashImage())) {
|
if (StringUtils.isNotEmpty(account.getSplashImage())) {
|
||||||
redisCache.setCacheObject(SPLASH_IMAGE_CACHE_KEY + username, account.getSplashImage());
|
redisCache.setCacheObject(SPLASH_IMAGE_CACHE_KEY + username, account.getSplashImage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新品牌logo缓存到 Redis
|
||||||
|
if (StringUtils.isNotEmpty(account.getBrandLogo())) {
|
||||||
|
redisCache.setCacheObject(BRAND_LOGO_CACHE_KEY + username, account.getBrandLogo());
|
||||||
|
}
|
||||||
|
|
||||||
String token = Jwts.builder()
|
String token = Jwts.builder()
|
||||||
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
@@ -189,6 +194,7 @@ public class ClientAccountController extends BaseController {
|
|||||||
.claim("accountId", account.getId())
|
.claim("accountId", account.getId())
|
||||||
.claim("username", username)
|
.claim("username", username)
|
||||||
.claim("clientId", clientId)
|
.claim("clientId", clientId)
|
||||||
|
.claim("registerTime", account.getCreateTime())
|
||||||
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
||||||
.compact();
|
.compact();
|
||||||
|
|
||||||
@@ -197,7 +203,8 @@ public class ClientAccountController extends BaseController {
|
|||||||
"permissions", account.getPermissions(),
|
"permissions", account.getPermissions(),
|
||||||
"accountName", account.getAccountName(),
|
"accountName", account.getAccountName(),
|
||||||
"expireTime", account.getExpireTime(),
|
"expireTime", account.getExpireTime(),
|
||||||
"accountType", account.getAccountType()
|
"accountType", account.getAccountType(),
|
||||||
|
"registerTime", account.getCreateTime()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -238,7 +245,8 @@ public class ClientAccountController extends BaseController {
|
|||||||
"accountName", account.getAccountName(),
|
"accountName", account.getAccountName(),
|
||||||
"expireTime", account.getExpireTime(),
|
"expireTime", account.getExpireTime(),
|
||||||
"accountType", account.getAccountType(),
|
"accountType", account.getAccountType(),
|
||||||
"deviceTrialExpired", deviceTrialExpired
|
"deviceTrialExpired", deviceTrialExpired,
|
||||||
|
"registerTime", account.getCreateTime()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,45 +274,38 @@ public class ClientAccountController extends BaseController {
|
|||||||
* 新账号注册赠送3天VIP试用期
|
* 新账号注册赠送3天VIP试用期
|
||||||
*/
|
*/
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public AjaxResult register(@RequestBody Map<String, String> registerData) {
|
public AjaxResult register(@RequestBody Map<String, String> data) {
|
||||||
String username = registerData.get("username");
|
String username = data.get("username");
|
||||||
String password = registerData.get("password");
|
ClientAccount account = new ClientAccount();
|
||||||
String deviceId = registerData.get("deviceId");
|
account.setUsername(username);
|
||||||
|
account.setAccountName(username);
|
||||||
ClientAccount clientAccount = new ClientAccount();
|
account.setCreateBy("system");
|
||||||
clientAccount.setUsername(username);
|
account.setCreateTime(new Date());
|
||||||
clientAccount.setAccountName(username);
|
account.setStatus("0");
|
||||||
clientAccount.setCreateBy("system");
|
account.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
|
||||||
clientAccount.setStatus("0");
|
account.setPassword(passwordEncoder.encode(data.get("password")));
|
||||||
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
|
account.setAccountType("trial");
|
||||||
clientAccount.setPassword(passwordEncoder.encode(password));
|
account.setDeviceLimit(1);
|
||||||
clientAccount.setAccountType("trial");
|
account.setExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
|
||||||
clientAccount.setDeviceLimit(1);
|
|
||||||
clientAccount.setExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
|
|
||||||
|
|
||||||
int result = clientAccountService.insertClientAccount(clientAccount);
|
if (clientAccountService.insertClientAccount(account) <= 0) return AjaxResult.error("注册失败");
|
||||||
if (result <= 0) {
|
|
||||||
return AjaxResult.error("注册失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
String token = Jwts.builder()
|
String token = Jwts.builder()
|
||||||
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
.setSubject(clientAccount.getUsername())
|
.setSubject(username)
|
||||||
.setIssuedAt(new Date())
|
.setIssuedAt(new Date())
|
||||||
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
|
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
|
||||||
.claim("accountId", clientAccount.getId())
|
.claim("accountId", account.getId())
|
||||||
.claim("username", username)
|
.claim("username", username)
|
||||||
.claim("clientId", deviceId)
|
.claim("clientId", data.get("deviceId"))
|
||||||
|
.claim("registerTime", account.getCreateTime())
|
||||||
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
||||||
.compact();
|
.compact();
|
||||||
|
|
||||||
return AjaxResult.success(Map.of(
|
return AjaxResult.success(Map.of("token", token, "permissions", account.getPermissions(),
|
||||||
"token", token,
|
"accountName", account.getAccountName(), "expireTime", account.getExpireTime(),
|
||||||
"permissions", clientAccount.getPermissions(),
|
"accountType", account.getAccountType(), "registerTime", account.getCreateTime()));
|
||||||
"accountName", clientAccount.getAccountName(),
|
|
||||||
"expireTime", clientAccount.getExpireTime(),
|
|
||||||
"accountType", clientAccount.getAccountType()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -402,7 +403,7 @@ public class ClientAccountController extends BaseController {
|
|||||||
try {
|
try {
|
||||||
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
if (account == null) return AjaxResult.error("账号不存在");
|
if (account == null) return AjaxResult.error("账号不存在");
|
||||||
|
|
||||||
account.setSplashImage(null);
|
account.setSplashImage(null);
|
||||||
clientAccountService.updateClientAccount(account);
|
clientAccountService.updateClientAccount(account);
|
||||||
redisCache.deleteObject(SPLASH_IMAGE_CACHE_KEY + username);
|
redisCache.deleteObject(SPLASH_IMAGE_CACHE_KEY + username);
|
||||||
@@ -413,4 +414,115 @@ public class ClientAccountController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传品牌logo
|
||||||
|
*/
|
||||||
|
@PostMapping("/brand-logo/upload")
|
||||||
|
public AjaxResult uploadBrandLogo(@RequestParam("file") MultipartFile file, @RequestParam("username") String username) {
|
||||||
|
try {
|
||||||
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
|
if (account == null) return AjaxResult.error("账号不存在");
|
||||||
|
if (!file.getContentType().startsWith("image/")) return AjaxResult.error("只支持图片文件");
|
||||||
|
if (file.getSize() > 5 * 1024 * 1024) return AjaxResult.error("图片大小不能超过5MB");
|
||||||
|
|
||||||
|
String fileName = "brand-logo/" + DateUtil.format(new Date(), "yyyy/MM/") + IdUtil.simpleUUID() + "." + FileUtil.extName(file.getOriginalFilename());
|
||||||
|
try (InputStream is = file.getInputStream()) {
|
||||||
|
Response res = uploadManager.put(is, fileName, auth.uploadToken(qiniu.getBucket()), null, "");
|
||||||
|
if (!res.isOK()) return AjaxResult.error("上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = qiniu.getResourcesUrl() + fileName;
|
||||||
|
account.setBrandLogo(url);
|
||||||
|
clientAccountService.updateClientAccount(account);
|
||||||
|
redisCache.setCacheObject(BRAND_LOGO_CACHE_KEY + username, url);
|
||||||
|
return AjaxResult.success().put("url", url).put("fileName", fileName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return AjaxResult.error("上传失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取品牌logo
|
||||||
|
*/
|
||||||
|
@GetMapping("/brand-logo")
|
||||||
|
public AjaxResult getBrandLogo(@RequestParam("username") String username) {
|
||||||
|
String url = redisCache.getCacheObject(BRAND_LOGO_CACHE_KEY + username);
|
||||||
|
if (StringUtils.isEmpty(url)) {
|
||||||
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
|
if (account != null && StringUtils.isNotEmpty(account.getBrandLogo())) {
|
||||||
|
url = account.getBrandLogo();
|
||||||
|
redisCache.setCacheObject(BRAND_LOGO_CACHE_KEY + username, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AjaxResult.success(Map.of("url", url != null ? url : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除品牌logo
|
||||||
|
*/
|
||||||
|
@PostMapping("/brand-logo/delete")
|
||||||
|
public AjaxResult deleteBrandLogo(@RequestParam("username") String username) {
|
||||||
|
try {
|
||||||
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
|
if (account == null) return AjaxResult.error("账号不存在");
|
||||||
|
|
||||||
|
account.setBrandLogo(null);
|
||||||
|
clientAccountService.updateClientAccount(account);
|
||||||
|
redisCache.deleteObject(BRAND_LOGO_CACHE_KEY + username);
|
||||||
|
return AjaxResult.success("品牌logo已删除");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return AjaxResult.error("删除失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传全局开屏图片
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:version:upload')")
|
||||||
|
@PostMapping("/global-splash-image/upload")
|
||||||
|
public AjaxResult uploadGlobalSplashImage(@RequestParam("file") MultipartFile file) {
|
||||||
|
try {
|
||||||
|
if (!file.getContentType().startsWith("image/")) return AjaxResult.error("只支持图片文件");
|
||||||
|
if (file.getSize() > 5 * 1024 * 1024) return AjaxResult.error("图片大小不能超过5MB");
|
||||||
|
|
||||||
|
String fileName = "splash/global/" + DateUtil.format(new Date(), "yyyy/MM/") + IdUtil.simpleUUID() + "." + FileUtil.extName(file.getOriginalFilename());
|
||||||
|
try (InputStream is = file.getInputStream()) {
|
||||||
|
Response res = uploadManager.put(is, fileName, auth.uploadToken(qiniu.getBucket()), null, "");
|
||||||
|
if (!res.isOK()) return AjaxResult.error("上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = qiniu.getResourcesUrl() + fileName;
|
||||||
|
redisCache.setCacheObject(GLOBAL_SPLASH_IMAGE_KEY, url);
|
||||||
|
return AjaxResult.success().put("url", url).put("fileName", fileName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return AjaxResult.error("上传失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全局开屏图片
|
||||||
|
*/
|
||||||
|
@GetMapping("/global-splash-image")
|
||||||
|
public AjaxResult getGlobalSplashImage() {
|
||||||
|
String url = redisCache.getCacheObject(GLOBAL_SPLASH_IMAGE_KEY);
|
||||||
|
return AjaxResult.success(Map.of("url", url != null ? url : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除全局开屏图片
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:version:upload')")
|
||||||
|
@PostMapping("/global-splash-image/delete")
|
||||||
|
public AjaxResult deleteGlobalSplashImage() {
|
||||||
|
try {
|
||||||
|
redisCache.deleteObject(GLOBAL_SPLASH_IMAGE_KEY);
|
||||||
|
return AjaxResult.success("全局开屏图片已删除");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return AjaxResult.error("删除失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,13 @@ import com.ruoyi.common.utils.StringUtils;
|
|||||||
import com.ruoyi.common.annotation.Log;
|
import com.ruoyi.common.annotation.Log;
|
||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.web.service.IClientAccountService;
|
||||||
|
import com.ruoyi.web.security.JwtRsaKeyService;
|
||||||
|
import com.ruoyi.system.domain.ClientAccount;
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -25,26 +32,43 @@ public class VersionController extends BaseController {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
|
@Autowired
|
||||||
|
private IClientAccountService clientAccountService;
|
||||||
|
@Autowired
|
||||||
|
private JwtRsaKeyService jwtRsaKeyService;
|
||||||
|
|
||||||
private static final String VERSION_REDIS_KEY = "erp:client:version";
|
private static final String VERSION_REDIS_KEY = "erp:client:version";
|
||||||
private static final String ASAR_URL_REDIS_KEY = "erp:client:asar_url";
|
private static final String ASAR_URL_REDIS_KEY = "erp:client:asar_url";
|
||||||
private static final String JAR_URL_REDIS_KEY = "erp:client:jar_url";
|
private static final String JAR_URL_REDIS_KEY = "erp:client:jar_url";
|
||||||
private static final String UPDATE_NOTES_REDIS_KEY = "erp:client:update_notes";
|
private static final String UPDATE_NOTES_REDIS_KEY = "erp:client:update_notes";
|
||||||
|
|
||||||
|
private static final String VERSION_BETA_REDIS_KEY = "erp:client:version:beta";
|
||||||
|
private static final String ASAR_URL_BETA_REDIS_KEY = "erp:client:asar_url:beta";
|
||||||
|
private static final String JAR_URL_BETA_REDIS_KEY = "erp:client:jar_url:beta";
|
||||||
|
private static final String UPDATE_NOTES_BETA_REDIS_KEY = "erp:client:update_notes:beta";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查版本更新
|
* 检查版本更新
|
||||||
*/
|
*/
|
||||||
@GetMapping("/check")
|
@GetMapping("/check")
|
||||||
public AjaxResult checkVersion(@RequestParam String currentVersion) {
|
public AjaxResult checkVersion(@RequestParam String currentVersion, HttpServletRequest request) {
|
||||||
String latestVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY);
|
boolean isBeta = canUseBetaVersion(request);
|
||||||
|
|
||||||
|
String versionKey = isBeta ? VERSION_BETA_REDIS_KEY : VERSION_REDIS_KEY;
|
||||||
|
String asarKey = isBeta ? ASAR_URL_BETA_REDIS_KEY : ASAR_URL_REDIS_KEY;
|
||||||
|
String jarKey = isBeta ? JAR_URL_BETA_REDIS_KEY : JAR_URL_REDIS_KEY;
|
||||||
|
String notesKey = isBeta ? UPDATE_NOTES_BETA_REDIS_KEY : UPDATE_NOTES_REDIS_KEY;
|
||||||
|
|
||||||
|
String latestVersion = redisTemplate.opsForValue().get(versionKey);
|
||||||
boolean needUpdate = compareVersions(currentVersion, latestVersion) < 0;
|
boolean needUpdate = compareVersions(currentVersion, latestVersion) < 0;
|
||||||
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("currentVersion", currentVersion);
|
data.put("currentVersion", currentVersion);
|
||||||
data.put("latestVersion", latestVersion);
|
data.put("latestVersion", latestVersion);
|
||||||
data.put("needUpdate", needUpdate);
|
data.put("needUpdate", needUpdate);
|
||||||
data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
data.put("asarUrl", redisTemplate.opsForValue().get(asarKey));
|
||||||
data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
data.put("jarUrl", redisTemplate.opsForValue().get(jarKey));
|
||||||
data.put("updateNotes", redisTemplate.opsForValue().get(UPDATE_NOTES_REDIS_KEY));
|
data.put("updateNotes", redisTemplate.opsForValue().get(notesKey));
|
||||||
|
|
||||||
return AjaxResult.success(data);
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
@@ -54,16 +78,22 @@ public class VersionController extends BaseController {
|
|||||||
@PreAuthorize("@ss.hasPermi('system:version:query')")
|
@PreAuthorize("@ss.hasPermi('system:version:query')")
|
||||||
@GetMapping("/info")
|
@GetMapping("/info")
|
||||||
public AjaxResult getVersionInfo() {
|
public AjaxResult getVersionInfo() {
|
||||||
String currentVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY);
|
|
||||||
if (StringUtils.isEmpty(currentVersion)) {
|
|
||||||
currentVersion = "2.0.0";
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("currentVersion", currentVersion);
|
|
||||||
data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
Map<String, Object> release = new HashMap<>();
|
||||||
data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
release.put("version", redisTemplate.opsForValue().get(VERSION_REDIS_KEY));
|
||||||
data.put("updateNotes", redisTemplate.opsForValue().get(UPDATE_NOTES_REDIS_KEY));
|
release.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
||||||
|
release.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
||||||
|
release.put("updateNotes", redisTemplate.opsForValue().get(UPDATE_NOTES_REDIS_KEY));
|
||||||
|
|
||||||
|
Map<String, Object> beta = new HashMap<>();
|
||||||
|
beta.put("version", redisTemplate.opsForValue().get(VERSION_BETA_REDIS_KEY));
|
||||||
|
beta.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_BETA_REDIS_KEY));
|
||||||
|
beta.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_BETA_REDIS_KEY));
|
||||||
|
beta.put("updateNotes", redisTemplate.opsForValue().get(UPDATE_NOTES_BETA_REDIS_KEY));
|
||||||
|
|
||||||
|
data.put("release", release);
|
||||||
|
data.put("beta", beta);
|
||||||
data.put("updateTime", System.currentTimeMillis());
|
data.put("updateTime", System.currentTimeMillis());
|
||||||
|
|
||||||
return AjaxResult.success(data);
|
return AjaxResult.success(data);
|
||||||
@@ -78,25 +108,52 @@ public class VersionController extends BaseController {
|
|||||||
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
|
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
|
||||||
@RequestParam(value = "asarUrl", required = false) String asarUrl,
|
@RequestParam(value = "asarUrl", required = false) String asarUrl,
|
||||||
@RequestParam(value = "jarUrl", required = false) String jarUrl,
|
@RequestParam(value = "jarUrl", required = false) String jarUrl,
|
||||||
@RequestParam("updateNotes") String updateNotes) {
|
@RequestParam("updateNotes") String updateNotes,
|
||||||
redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version);
|
@RequestParam(value = "isBeta", defaultValue = "false") Boolean isBeta) {
|
||||||
|
String versionKey = isBeta ? VERSION_BETA_REDIS_KEY : VERSION_REDIS_KEY;
|
||||||
|
String asarKey = isBeta ? ASAR_URL_BETA_REDIS_KEY : ASAR_URL_REDIS_KEY;
|
||||||
|
String jarKey = isBeta ? JAR_URL_BETA_REDIS_KEY : JAR_URL_REDIS_KEY;
|
||||||
|
String notesKey = isBeta ? UPDATE_NOTES_BETA_REDIS_KEY : UPDATE_NOTES_REDIS_KEY;
|
||||||
|
|
||||||
|
redisTemplate.opsForValue().set(versionKey, version);
|
||||||
if (StringUtils.isNotEmpty(asarUrl)) {
|
if (StringUtils.isNotEmpty(asarUrl)) {
|
||||||
redisTemplate.opsForValue().set(ASAR_URL_REDIS_KEY, asarUrl);
|
redisTemplate.opsForValue().set(asarKey, asarUrl);
|
||||||
}
|
}
|
||||||
if (StringUtils.isNotEmpty(jarUrl)) {
|
if (StringUtils.isNotEmpty(jarUrl)) {
|
||||||
redisTemplate.opsForValue().set(JAR_URL_REDIS_KEY, jarUrl);
|
redisTemplate.opsForValue().set(jarKey, jarUrl);
|
||||||
}
|
}
|
||||||
redisTemplate.opsForValue().set(UPDATE_NOTES_REDIS_KEY, updateNotes);
|
redisTemplate.opsForValue().set(notesKey, updateNotes);
|
||||||
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("version", version);
|
data.put("version", version);
|
||||||
data.put("asarUrl", asarUrl);
|
data.put("asarUrl", asarUrl);
|
||||||
data.put("jarUrl", jarUrl);
|
data.put("jarUrl", jarUrl);
|
||||||
data.put("updateNotes", updateNotes);
|
data.put("updateNotes", updateNotes);
|
||||||
|
data.put("isBeta", isBeta);
|
||||||
data.put("updateTime", System.currentTimeMillis());
|
data.put("updateTime", System.currentTimeMillis());
|
||||||
return AjaxResult.success(data);
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canUseBetaVersion(HttpServletRequest request) {
|
||||||
|
try {
|
||||||
|
String token = request.getHeader("Authorization");
|
||||||
|
if (StringUtils.isEmpty(token)) return false;
|
||||||
|
if (token.startsWith("Bearer ")) token = token.substring(7);
|
||||||
|
|
||||||
|
String username = (String) Jwts.parser()
|
||||||
|
.setSigningKey(jwtRsaKeyService.getPublicKey())
|
||||||
|
.parseClaimsJws(token).getBody().get("sub");
|
||||||
|
|
||||||
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
|
if (account == null) return false;
|
||||||
|
|
||||||
|
JSONObject perms = JSON.parseObject(account.getPermissions());
|
||||||
|
return perms != null && perms.getBooleanValue("beta_version");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 比较版本号
|
* 比较版本号
|
||||||
* @param version1 版本1
|
* @param version1 版本1
|
||||||
@@ -107,8 +164,12 @@ public class VersionController extends BaseController {
|
|||||||
if (StringUtils.isEmpty(version1) || StringUtils.isEmpty(version2)) {
|
if (StringUtils.isEmpty(version1) || StringUtils.isEmpty(version2)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
String[] v1Parts = version1.split("\\.");
|
|
||||||
String[] v2Parts = version2.split("\\.");
|
String v1 = version1.replace("-beta", "");
|
||||||
|
String v2 = version2.replace("-beta", "");
|
||||||
|
|
||||||
|
String[] v1Parts = v1.split("\\.");
|
||||||
|
String[] v2Parts = v2.split("\\.");
|
||||||
|
|
||||||
int maxLength = Math.max(v1Parts.length, v2Parts.length);
|
int maxLength = Math.max(v1Parts.length, v2Parts.length);
|
||||||
|
|
||||||
|
|||||||
@@ -87,8 +87,6 @@ public class SysLoginController
|
|||||||
ajax.put("user", user);
|
ajax.put("user", user);
|
||||||
ajax.put("roles", roles);
|
ajax.put("roles", roles);
|
||||||
ajax.put("permissions", permissions);
|
ajax.put("permissions", permissions);
|
||||||
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
|
|
||||||
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
|
|
||||||
return ajax;
|
return ajax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class MarkController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Token
|
* 获取 Token
|
||||||
* 如果 Redis 中不存在 Token,自动注册新账号
|
* 如果 Redis 中不存在 Token 或 Token 不可用,自动注册新账号
|
||||||
*
|
*
|
||||||
* @return Token 字符串
|
* @return Token 字符串
|
||||||
*/
|
*/
|
||||||
@@ -37,11 +37,11 @@ public class MarkController {
|
|||||||
// 先尝试从 Redis 获取现有 Token
|
// 先尝试从 Redis 获取现有 Token
|
||||||
String token = redisCache.getCacheMapValue(CacheConstants.MARK_ACCOUNT_KEY, "token");
|
String token = redisCache.getCacheMapValue(CacheConstants.MARK_ACCOUNT_KEY, "token");
|
||||||
|
|
||||||
if (token != null && !token.isEmpty()) {
|
// 验证 Token 是否可用
|
||||||
|
if (token != null && !token.isEmpty() && markService.validateToken(token)) {
|
||||||
return AjaxResult.success("获取成功", token);
|
return AjaxResult.success("获取成功", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token 不存在,自动注册新账号
|
|
||||||
token = markService.reg();
|
token = markService.reg();
|
||||||
return AjaxResult.success("注册成功", token);
|
return AjaxResult.success("注册成功", token);
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,6 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> authenticateClient(String authKey, Map<String, Object> clientInfo) {
|
public Map<String, Object> authenticateClient(String authKey, Map<String, Object> clientInfo) {
|
||||||
Map<String, Object> result = new HashMap<>();
|
|
||||||
try {
|
try {
|
||||||
String accessToken = UUID.randomUUID().toString().replace("-", "");
|
String accessToken = UUID.randomUUID().toString().replace("-", "");
|
||||||
String clientId = (String) clientInfo.get("clientId");
|
String clientId = (String) clientInfo.get("clientId");
|
||||||
@@ -194,16 +193,18 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
|
|||||||
clientMonitorMapper.updateClientOnlineStatus(clientId, "1");
|
clientMonitorMapper.updateClientOnlineStatus(clientId, "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("success", true);
|
result.put("success", true);
|
||||||
result.put("accessToken", accessToken);
|
result.put("accessToken", accessToken);
|
||||||
result.put("tokenType", "Bearer");
|
result.put("tokenType", "Bearer");
|
||||||
result.put("expiresIn", 7200);
|
result.put("expiresIn", 7200);
|
||||||
result.put("clientId", clientId);
|
result.put("clientId", clientId);
|
||||||
result.put("permissions", null);
|
result.put("permissions", null);
|
||||||
|
return result;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("认证失败: " + e.getMessage());
|
logger.error("认证失败: {}", e.getMessage(), e);
|
||||||
|
throw new RuntimeException("认证失败", e);
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientInfo findClientByClientId(String clientId) {
|
private ClientInfo findClientByClientId(String clientId) {
|
||||||
@@ -225,17 +226,22 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void recordErrorReport(Map<String, Object> errorData) {
|
public void recordErrorReport(Map<String, Object> errorData) {
|
||||||
ClientErrorReport errorReport = new ClientErrorReport();
|
try {
|
||||||
errorReport.setClientId((String) errorData.get("clientId"));
|
ClientErrorReport errorReport = new ClientErrorReport();
|
||||||
errorReport.setErrorType((String) errorData.get("errorType"));
|
errorReport.setClientId((String) errorData.get("clientId"));
|
||||||
errorReport.setErrorMessage((String) errorData.get("errorMessage"));
|
errorReport.setErrorType((String) errorData.get("errorType"));
|
||||||
errorReport.setStackTrace((String) errorData.get("stackTrace"));
|
errorReport.setErrorMessage((String) errorData.get("errorMessage"));
|
||||||
errorReport.setErrorTime(DateUtils.getNowDate());
|
errorReport.setStackTrace((String) errorData.get("stackTrace"));
|
||||||
errorReport.setUsername((String) errorData.get("username"));
|
errorReport.setErrorTime(DateUtils.getNowDate());
|
||||||
errorReport.setOsName((String) errorData.get("osName"));
|
errorReport.setUsername((String) errorData.get("username"));
|
||||||
errorReport.setOsVersion((String) errorData.get("osVersion"));
|
errorReport.setOsName((String) errorData.get("osName"));
|
||||||
errorReport.setAppVersion((String) errorData.get("appVersion"));
|
errorReport.setOsVersion((String) errorData.get("osVersion"));
|
||||||
clientMonitorMapper.insertClientError(errorReport);
|
errorReport.setAppVersion((String) errorData.get("appVersion"));
|
||||||
|
clientMonitorMapper.insertClientError(errorReport);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("记录错误报告失败: {}", e.getMessage(), e);
|
||||||
|
throw new RuntimeException("记录错误报告失败", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,7 +252,8 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
|
|||||||
try {
|
try {
|
||||||
String clientId = (String) dataReport.get("clientId");
|
String clientId = (String) dataReport.get("clientId");
|
||||||
String dataType = normalizeDataType((String) dataReport.get("dataType"));
|
String dataType = normalizeDataType((String) dataReport.get("dataType"));
|
||||||
String status = (String) dataReport.get("status");
|
Object statusObj = dataReport.get("status");
|
||||||
|
String status = statusObj != null ? String.valueOf(statusObj) : "0";
|
||||||
int dataCount = parseInteger(dataReport.get("dataCount"), 1);
|
int dataCount = parseInteger(dataReport.get("dataCount"), 1);
|
||||||
|
|
||||||
ClientDataReport existingReport = clientMonitorMapper.findRecentDataReport(clientId, dataType, status);
|
ClientDataReport existingReport = clientMonitorMapper.findRecentDataReport(clientId, dataType, status);
|
||||||
@@ -262,11 +269,15 @@ public class ClientMonitorServiceImpl implements IClientMonitorService {
|
|||||||
report.setStatus(status);
|
report.setStatus(status);
|
||||||
clientMonitorMapper.insertDataReport(report);
|
clientMonitorMapper.insertDataReport(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientId != null && !clientId.isEmpty()) {
|
if (clientId != null && !clientId.isEmpty()) {
|
||||||
clientMonitorMapper.updateClientOnlineStatus(clientId, "1");
|
try {
|
||||||
|
clientMonitorMapper.updateClientOnlineStatus(clientId, "1");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
logger.error("记录数据采集报告失败: {}", e.getMessage(), e);
|
||||||
|
throw new RuntimeException("记录数据采集报告失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ qiniu:
|
|||||||
accessKey: M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF
|
accessKey: M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF
|
||||||
secretKey: Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl
|
secretKey: Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl
|
||||||
# 七牛空间名
|
# 七牛空间名
|
||||||
bucket: pxdj-prod
|
bucket: bydj-prod
|
||||||
# 资源地址
|
# 资源地址
|
||||||
resourcesUrl: https://qiniu.pxdj.tashowz.com/
|
resourcesUrl: https://qiniu.bydj.tashowz.com/
|
||||||
# 七牛云机房
|
# 七牛云机房
|
||||||
zone: HUA_NAN
|
zone: HUA_NAN
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
shop.qiniu.resourcesUrl=https://qiniu.pxdj.tashowz.com/
|
shop.qiniu.resourcesUrl=https://qiniu.bydj.tashowz.com/
|
||||||
shop.qiniu.accessKey=M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF
|
shop.qiniu.accessKey=M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF
|
||||||
shop.qiniu.secretKey=Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl
|
shop.qiniu.secretKey=Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl
|
||||||
shop.qiniu.bucket=pxdj-prod
|
shop.qiniu.bucket=bydj-prod
|
||||||
# \u5177\u4F53\u67E5\u770BQiniuZone.java
|
# \u5177\u4F53\u67E5\u770BQiniuZone.java
|
||||||
shop.qiniu.zone=HUA_NAN
|
shop.qiniu.zone=HUA_NAN
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ public class ClientAccount extends BaseEntity
|
|||||||
/** 开屏图片URL */
|
/** 开屏图片URL */
|
||||||
private String splashImage;
|
private String splashImage;
|
||||||
|
|
||||||
|
/** 品牌logo URL */
|
||||||
|
private String brandLogo;
|
||||||
|
|
||||||
public void setId(Long id)
|
public void setId(Long id)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -174,4 +177,14 @@ public class ClientAccount extends BaseEntity
|
|||||||
{
|
{
|
||||||
return splashImage;
|
return splashImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBrandLogo(String brandLogo)
|
||||||
|
{
|
||||||
|
this.brandLogo = brandLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrandLogo()
|
||||||
|
{
|
||||||
|
return brandLogo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,5 +21,12 @@ public interface IMarkService {
|
|||||||
* 登录
|
* 登录
|
||||||
*/
|
*/
|
||||||
String login();
|
String login();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证Token是否可用
|
||||||
|
* @param token 待验证的Token
|
||||||
|
* @return true=可用, false=不可用
|
||||||
|
*/
|
||||||
|
boolean validateToken(String token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,5 +124,29 @@ public class MarkServiceImpl implements IMarkService {
|
|||||||
throw new RuntimeException("MD5加密失败", e);
|
throw new RuntimeException("MD5加密失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
try {
|
||||||
|
String data = "{\"name\":\"\",\"page_size\":1,\"current_page\":1}";
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||||
|
formData.add("c", "TaskPageList");
|
||||||
|
formData.add("d", data);
|
||||||
|
formData.add("t", token);
|
||||||
|
formData.add("s", md5(ts + data + API_SECRET));
|
||||||
|
formData.add("ts", String.valueOf(ts));
|
||||||
|
formData.add("website", "1");
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
|
||||||
|
String result = restTemplate.postForObject("https://api.fangzhoujingxuan.com/Task", requestEntity, String.class);
|
||||||
|
JsonNode json = objectMapper.readTree(result);
|
||||||
|
int statusCode = json.get("S").asInt();
|
||||||
|
return statusCode == 1;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="deviceLimit" column="device_limit" />
|
<result property="deviceLimit" column="device_limit" />
|
||||||
<result property="accountType" column="account_type" />
|
<result property="accountType" column="account_type" />
|
||||||
<result property="splashImage" column="splash_image" />
|
<result property="splashImage" column="splash_image" />
|
||||||
|
<result property="brandLogo" column="brand_logo" />
|
||||||
<result property="createBy" column="create_by" />
|
<result property="createBy" column="create_by" />
|
||||||
<result property="createTime" column="create_time" />
|
<result property="createTime" column="create_time" />
|
||||||
<result property="updateBy" column="update_by" />
|
<result property="updateBy" column="update_by" />
|
||||||
@@ -25,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<sql id="selectClientAccountVo">
|
<sql id="selectClientAccountVo">
|
||||||
select id, account_name, username, password, status, expire_time,
|
select id, account_name, username, password, status, expire_time,
|
||||||
allowed_ip_range, remark, permissions, device_limit, account_type, splash_image, create_by, create_time, update_by, update_time
|
allowed_ip_range, remark, permissions, device_limit, account_type, splash_image, brand_logo, create_by, create_time, update_by, update_time
|
||||||
from client_account
|
from client_account
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="deviceLimit != null">device_limit,</if>
|
<if test="deviceLimit != null">device_limit,</if>
|
||||||
<if test="accountType != null">account_type,</if>
|
<if test="accountType != null">account_type,</if>
|
||||||
<if test="splashImage != null">splash_image,</if>
|
<if test="splashImage != null">splash_image,</if>
|
||||||
|
<if test="brandLogo != null">brand_logo,</if>
|
||||||
<if test="createBy != null">create_by,</if>
|
<if test="createBy != null">create_by,</if>
|
||||||
create_time
|
create_time
|
||||||
</trim>
|
</trim>
|
||||||
@@ -78,6 +80,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="deviceLimit != null">#{deviceLimit},</if>
|
<if test="deviceLimit != null">#{deviceLimit},</if>
|
||||||
<if test="accountType != null">#{accountType},</if>
|
<if test="accountType != null">#{accountType},</if>
|
||||||
<if test="splashImage != null">#{splashImage},</if>
|
<if test="splashImage != null">#{splashImage},</if>
|
||||||
|
<if test="brandLogo != null">#{brandLogo},</if>
|
||||||
<if test="createBy != null">#{createBy},</if>
|
<if test="createBy != null">#{createBy},</if>
|
||||||
sysdate()
|
sysdate()
|
||||||
</trim>
|
</trim>
|
||||||
@@ -96,7 +99,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="permissions != null">permissions = #{permissions},</if>
|
<if test="permissions != null">permissions = #{permissions},</if>
|
||||||
<if test="deviceLimit != null">device_limit = #{deviceLimit},</if>
|
<if test="deviceLimit != null">device_limit = #{deviceLimit},</if>
|
||||||
<if test="accountType != null">account_type = #{accountType},</if>
|
<if test="accountType != null">account_type = #{accountType},</if>
|
||||||
<if test="splashImage != null">splash_image = #{splashImage},</if>
|
splash_image = #{splashImage},
|
||||||
|
brand_logo = #{brandLogo},
|
||||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
update_time = sysdate()
|
update_time = sysdate()
|
||||||
</trim>
|
</trim>
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
const { run } = require('runjs')
|
|
||||||
const chalk = require('chalk')
|
|
||||||
const config = require('../vue.config.js')
|
|
||||||
const rawArgv = process.argv.slice(2)
|
|
||||||
const args = rawArgv.join(' ')
|
|
||||||
|
|
||||||
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
|
|
||||||
const report = rawArgv.includes('--report')
|
|
||||||
|
|
||||||
run(`vue-cli-service build ${args}`)
|
|
||||||
|
|
||||||
const port = 9526
|
|
||||||
const publicPath = config.publicPath
|
|
||||||
|
|
||||||
var connect = require('connect')
|
|
||||||
var serveStatic = require('serve-static')
|
|
||||||
const app = connect()
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
publicPath,
|
|
||||||
serveStatic('./dist', {
|
|
||||||
index: ['index.html', '/']
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
app.listen(port, function () {
|
|
||||||
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
|
|
||||||
if (report) {
|
|
||||||
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
run(`vue-cli-service build ${args}`)
|
|
||||||
}
|
|
||||||
31
ruoyi-ui/src/api/monitor/splash.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 上传全局开屏图片
|
||||||
|
export function uploadGlobalSplashImage(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return request({
|
||||||
|
url: '/monitor/account/global-splash-image/upload',
|
||||||
|
method: 'post',
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取全局开屏图片
|
||||||
|
export function getGlobalSplashImage() {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/account/global-splash-image',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除全局开屏图片
|
||||||
|
export function deleteGlobalSplashImage() {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/account/global-splash-image/delete',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ export function uploadFile(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新版本信息和下载链接
|
// 更新版本信息和下载链接
|
||||||
// data: { version, asarUrl, jarUrl }
|
// data: { version, asarUrl, jarUrl, updateNotes, isBeta }
|
||||||
export function updateVersion(data) {
|
export function updateVersion(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/version/update',
|
url: '/system/version/update',
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
<el-checkbox label="toolbox"><i class="el-icon-box"></i> 工具箱功能</el-checkbox>
|
<el-checkbox label="toolbox"><i class="el-icon-box"></i> 工具箱功能</el-checkbox>
|
||||||
<el-checkbox label="dataCollection"><i class="el-icon-document-copy"></i> 数据采集功能</el-checkbox>
|
<el-checkbox label="dataCollection"><i class="el-icon-document-copy"></i> 数据采集功能</el-checkbox>
|
||||||
<el-checkbox label="priceCompare"><i class="el-icon-price-tag"></i> 1688比价功能</el-checkbox>
|
<el-checkbox label="priceCompare"><i class="el-icon-price-tag"></i> 1688比价功能</el-checkbox>
|
||||||
|
<el-checkbox label="beta_version"><i class="el-icon-warning-outline"></i> 测试版更新权限</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|||||||
@@ -6,30 +6,100 @@
|
|||||||
<el-button type="success" icon="el-icon-upload" size="mini" @click="handleUpload" v-hasPermi="['system:version:upload']">上传新版本</el-button>
|
<el-button type="success" icon="el-icon-upload" size="mini" @click="handleUpload" v-hasPermi="['system:version:upload']">上传新版本</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<!-- 版本信息卡片 -->
|
|
||||||
<el-row class="mb8">
|
<!-- 开屏图片设置卡片 -->
|
||||||
<el-col s>
|
<el-row :gutter="20" class="mb8">
|
||||||
|
<el-col :span="24">
|
||||||
<el-card class="box-card">
|
<el-card class="box-card">
|
||||||
<div slot="header" class="clearfix">
|
<div slot="header" class="clearfix">
|
||||||
<span>当前版本信息</span>
|
<span>开屏图片设置</span>
|
||||||
|
<span style="color: #909399; font-size: 12px; margin-left: 10px;">(客户端启动时显示的图片)</span>
|
||||||
|
</div>
|
||||||
|
<div class="splash-setting">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="splash-preview">
|
||||||
|
<div class="preview-label">当前开屏图片</div>
|
||||||
|
<div class="preview-box" v-loading="splashLoading">
|
||||||
|
<img v-if="globalSplashImage" :src="globalSplashImage" alt="开屏图片" class="preview-image" />
|
||||||
|
<div v-else class="preview-empty">
|
||||||
|
<i class="el-icon-picture-outline"></i>
|
||||||
|
<span>暂未设置</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<div class="splash-actions">
|
||||||
|
<el-upload
|
||||||
|
ref="splashUpload"
|
||||||
|
action="#"
|
||||||
|
:limit="1"
|
||||||
|
accept="image/*"
|
||||||
|
:show-file-list="false"
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="handleSplashFileChange">
|
||||||
|
<el-button type="primary" icon="el-icon-upload" :loading="splashUploading">
|
||||||
|
{{ splashUploading ? '上传中...' : '上传开屏图片' }}
|
||||||
|
</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDeleteSplash"
|
||||||
|
:disabled="!globalSplashImage"
|
||||||
|
style="margin-left: 10px;">
|
||||||
|
删除图片
|
||||||
|
</el-button>
|
||||||
|
<div class="upload-tip">
|
||||||
|
<p>建议尺寸:1200 x 800 像素,支持 jpg/png/gif 格式,大小不超过 5MB</p>
|
||||||
|
<p>此图片将作为所有客户端的默认开屏图片</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 版本信息卡片 -->
|
||||||
|
<el-row :gutter="20" class="mb8">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>正式版</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item label="当前版本">
|
<el-descriptions-item label="版本号">
|
||||||
<el-tag type="primary" size="medium">{{ versionInfo.currentVersion }}</el-tag>
|
<el-tag type="success" size="medium">{{ versionInfo.release.version || '未设置' }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="更新时间">
|
<el-descriptions-item label="ASAR链接" v-if="versionInfo.release.asarUrl">
|
||||||
{{ parseTime(versionInfo.updateTime) }}
|
<el-link :href="versionInfo.release.asarUrl" target="_blank" type="primary">{{ versionInfo.release.asarUrl }}</el-link>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="ASAR下载链接" v-if="versionInfo.asarUrl">
|
<el-descriptions-item label="JAR链接" v-if="versionInfo.release.jarUrl">
|
||||||
<el-link :href="versionInfo.asarUrl" target="_blank" type="primary">
|
<el-link :href="versionInfo.release.jarUrl" target="_blank" type="success">{{ versionInfo.release.jarUrl }}</el-link>
|
||||||
{{ versionInfo.asarUrl }}
|
|
||||||
</el-link>
|
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="JAR下载链接" v-if="versionInfo.jarUrl">
|
</el-descriptions>
|
||||||
<el-link :href="versionInfo.jarUrl" target="_blank" type="success">
|
</div>
|
||||||
{{ versionInfo.jarUrl }}
|
</el-card>
|
||||||
</el-link>
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>测试版</span>
|
||||||
|
</div>
|
||||||
|
<div class="version-info">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="版本号">
|
||||||
|
<el-tag type="warning" size="medium">{{ versionInfo.beta.version || '未设置' }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="ASAR链接" v-if="versionInfo.beta.asarUrl">
|
||||||
|
<el-link :href="versionInfo.beta.asarUrl" target="_blank" type="primary">{{ versionInfo.beta.asarUrl }}</el-link>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="JAR链接" v-if="versionInfo.beta.jarUrl">
|
||||||
|
<el-link :href="versionInfo.beta.jarUrl" target="_blank" type="success">{{ versionInfo.beta.jarUrl }}</el-link>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,8 +110,14 @@
|
|||||||
<!-- 上传对话框 -->
|
<!-- 上传对话框 -->
|
||||||
<el-dialog title="上传新版本" :visible.sync="uploadVisible" width="500px" append-to-body>
|
<el-dialog title="上传新版本" :visible.sync="uploadVisible" width="500px" append-to-body>
|
||||||
<el-form ref="uploadForm" :model="uploadForm" :rules="uploadRules" label-width="100px">
|
<el-form ref="uploadForm" :model="uploadForm" :rules="uploadRules" label-width="100px">
|
||||||
|
<el-form-item label="版本类型" prop="isBeta">
|
||||||
|
<el-radio-group v-model="uploadForm.isBeta">
|
||||||
|
<el-radio :label="false">正式版</el-radio>
|
||||||
|
<el-radio :label="true">测试版</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="版本号" prop="version">
|
<el-form-item label="版本号" prop="version">
|
||||||
<el-input v-model="uploadForm.version" placeholder="请输入版本号,如:2.4.7"></el-input>
|
<el-input v-model="uploadForm.version" :placeholder="uploadForm.isBeta ? '请输入版本号,如:2.4.9-beta' : '请输入版本号,如:2.4.7'"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="更新内容" prop="updateNotes">
|
<el-form-item label="更新内容" prop="updateNotes">
|
||||||
<el-input v-model="uploadForm.updateNotes" type="textarea" :rows="4" placeholder="请输入更新内容"></el-input>
|
<el-input v-model="uploadForm.updateNotes" type="textarea" :rows="4" placeholder="请输入更新内容"></el-input>
|
||||||
@@ -87,6 +163,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getVersionInfo, uploadFile, updateVersion } from "@/api/monitor/version"
|
import { getVersionInfo, uploadFile, updateVersion } from "@/api/monitor/version"
|
||||||
|
import { getGlobalSplashImage, uploadGlobalSplashImage, deleteGlobalSplashImage } from "@/api/monitor/splash"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Version",
|
name: "Version",
|
||||||
@@ -98,10 +175,9 @@ export default {
|
|||||||
showSearch: true,
|
showSearch: true,
|
||||||
// 版本信息
|
// 版本信息
|
||||||
versionInfo: {
|
versionInfo: {
|
||||||
currentVersion: '2.0.0',
|
release: { version: null, asarUrl: null, jarUrl: null, updateNotes: null },
|
||||||
updateTime: null,
|
beta: { version: null, asarUrl: null, jarUrl: null, updateNotes: null },
|
||||||
asarUrl: null,
|
updateTime: null
|
||||||
jarUrl: null
|
|
||||||
},
|
},
|
||||||
// 版本检查表单
|
// 版本检查表单
|
||||||
checkForm: {
|
checkForm: {
|
||||||
@@ -115,6 +191,7 @@ export default {
|
|||||||
uploadVisible: false,
|
uploadVisible: false,
|
||||||
// 上传表单
|
// 上传表单
|
||||||
uploadForm: {
|
uploadForm: {
|
||||||
|
isBeta: false,
|
||||||
version: '',
|
version: '',
|
||||||
updateNotes: '',
|
updateNotes: '',
|
||||||
asarFile: null,
|
asarFile: null,
|
||||||
@@ -124,7 +201,16 @@ export default {
|
|||||||
uploadRules: {
|
uploadRules: {
|
||||||
version: [
|
version: [
|
||||||
{ required: true, message: "版本号不能为空", trigger: "blur" },
|
{ required: true, message: "版本号不能为空", trigger: "blur" },
|
||||||
{ pattern: /^\d+\.\d+\.\d+$/, message: "版本号格式不正确,应为x.y.z格式", trigger: "blur" }
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (!/^\d+\.\d+\.\d+(-beta)?$/.test(value)) {
|
||||||
|
callback(new Error('版本号格式不正确,应为x.y.z或x.y.z-beta格式'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
updateNotes: [
|
updateNotes: [
|
||||||
{ required: true, message: "更新内容不能为空", trigger: "blur" }
|
{ required: true, message: "更新内容不能为空", trigger: "blur" }
|
||||||
@@ -136,13 +222,72 @@ export default {
|
|||||||
// 上传加载状态
|
// 上传加载状态
|
||||||
uploadLoading: false,
|
uploadLoading: false,
|
||||||
// 上传进度
|
// 上传进度
|
||||||
uploadProgress: 0
|
uploadProgress: 0,
|
||||||
|
// 开屏图片相关
|
||||||
|
globalSplashImage: '',
|
||||||
|
splashLoading: false,
|
||||||
|
splashUploading: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getVersionInfo();
|
this.getVersionInfo();
|
||||||
|
this.getGlobalSplashImage();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** 获取全局开屏图片 */
|
||||||
|
getGlobalSplashImage() {
|
||||||
|
this.splashLoading = true;
|
||||||
|
getGlobalSplashImage().then(response => {
|
||||||
|
this.splashLoading = false;
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
this.globalSplashImage = response.data.url || '';
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.splashLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 处理开屏图片文件选择 */
|
||||||
|
handleSplashFileChange(file) {
|
||||||
|
if (!file || !file.raw) return;
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
if (!file.raw.type.startsWith('image/')) {
|
||||||
|
this.$modal.msgError("只支持图片文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 验证文件大小
|
||||||
|
if (file.raw.size > 5 * 1024 * 1024) {
|
||||||
|
this.$modal.msgError("图片大小不能超过5MB");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.splashUploading = true;
|
||||||
|
uploadGlobalSplashImage(file.raw).then(response => {
|
||||||
|
this.splashUploading = false;
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.$modal.msgSuccess("开屏图片上传成功");
|
||||||
|
this.globalSplashImage = response.url;
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(response.msg || "上传失败");
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.splashUploading = false;
|
||||||
|
this.$modal.msgError("上传失败");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 删除开屏图片 */
|
||||||
|
handleDeleteSplash() {
|
||||||
|
this.$modal.confirm('确定要删除全局开屏图片吗?删除后客户端将使用默认图片。').then(() => {
|
||||||
|
deleteGlobalSplashImage().then(response => {
|
||||||
|
if (response.code === 200) {
|
||||||
|
this.$modal.msgSuccess("删除成功");
|
||||||
|
this.globalSplashImage = '';
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(response.msg || "删除失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
/** 查询版本信息 */
|
/** 查询版本信息 */
|
||||||
getVersionInfo() {
|
getVersionInfo() {
|
||||||
getVersionInfo().then(response => {
|
getVersionInfo().then(response => {
|
||||||
@@ -158,6 +303,7 @@ export default {
|
|||||||
/** 重置上传表单 */
|
/** 重置上传表单 */
|
||||||
resetUploadForm() {
|
resetUploadForm() {
|
||||||
this.uploadForm = {
|
this.uploadForm = {
|
||||||
|
isBeta: false,
|
||||||
version: '',
|
version: '',
|
||||||
updateNotes: '',
|
updateNotes: '',
|
||||||
asarFile: null,
|
asarFile: null,
|
||||||
@@ -260,7 +406,8 @@ export default {
|
|||||||
version: this.uploadForm.version,
|
version: this.uploadForm.version,
|
||||||
asarUrl: asarUrl,
|
asarUrl: asarUrl,
|
||||||
jarUrl: jarUrl,
|
jarUrl: jarUrl,
|
||||||
updateNotes: this.uploadForm.updateNotes
|
updateNotes: this.uploadForm.updateNotes,
|
||||||
|
isBeta: this.uploadForm.isBeta
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$modal.msgSuccess("版本文件上传成功");
|
this.$modal.msgSuccess("版本文件上传成功");
|
||||||
this.uploadVisible = false;
|
this.uploadVisible = false;
|
||||||
@@ -286,4 +433,54 @@ export default {
|
|||||||
.box-card {
|
.box-card {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
/* 开屏图片设置样式 */
|
||||||
|
.splash-setting {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.splash-preview {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.preview-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.preview-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #fafafa;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.preview-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.preview-empty i {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.splash-actions {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
.upload-tip {
|
||||||
|
margin-top: 15px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.upload-tip p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"Bandelt\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2021-06-27\",\"registration_date\":\"2022-07-12\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"Magic Ants\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2018-04-09\",\"registration_date\":\"2019-02-12\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"VWALK\",\"brand_type\":\"TM\",\"status_code\":\"Dead\",\"filing_date\":\"2020-08-18\",\"registration_date\":\"\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"TUPWEL\",\"brand_type\":\"TM\",\"status_code\":\"Dead\",\"filing_date\":\"\",\"registration_date\":\"\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"Muruseni\",\"brand_type\":\"多品类\",\"status_code\":\"\",\"filing_date\":\"\",\"registration_date\":\"\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"FLYPROFiber\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2020-07-29\",\"registration_date\":\"2021-03-09\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"LECPECON\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2020-12-28\",\"registration_date\":\"2021-10-19\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"SquEqu\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2023-01-18\",\"registration_date\":\"2024-02-06\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"pechpell\",\"brand_type\":\"TM\",\"status_code\":\"Live\",\"filing_date\":\"2024-09-27\",\"registration_date\":\"\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"Arefic\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2022-09-08\",\"registration_date\":\"2023-10-17\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"Tiga\",\"brand_type\":\"多品类\",\"status_code\":\"\",\"filing_date\":\"\",\"registration_date\":\"\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"uxcell\",\"brand_type\":\"多品类\",\"status_code\":\"\",\"filing_date\":\"\",\"registration_date\":\"\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand_info": "{\"brand_name\":\"CJRSLRB\",\"brand_type\":\"R\",\"status_code\":\"Live\",\"filing_date\":\"2014-11-28\",\"registration_date\":\"2015-07-21\"}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,412 +0,0 @@
|
|||||||
{
|
|
||||||
"trademarks" : [ {
|
|
||||||
"status" : {
|
|
||||||
"staff" : {
|
|
||||||
"examiner" : {
|
|
||||||
"number" : null
|
|
||||||
},
|
|
||||||
"paralegal" : null,
|
|
||||||
"ituParalegal" : null,
|
|
||||||
"lie" : null,
|
|
||||||
"chargeTo" : null
|
|
||||||
},
|
|
||||||
"correspondence" : {
|
|
||||||
"freeFormAddress" : [ ],
|
|
||||||
"address" : {
|
|
||||||
"line1" : "625 SLATERS LANE, FOURTH FLOOR",
|
|
||||||
"city" : "ALEXANDRIA",
|
|
||||||
"region" : {
|
|
||||||
"stateCountry" : {
|
|
||||||
"code" : "VA",
|
|
||||||
"name" : "VIRGINIA"
|
|
||||||
},
|
|
||||||
"isoRegion" : {
|
|
||||||
"code" : "VA",
|
|
||||||
"name" : "VIRGINIA"
|
|
||||||
},
|
|
||||||
"iso" : {
|
|
||||||
"code" : "US",
|
|
||||||
"name" : "UNITED STATES"
|
|
||||||
},
|
|
||||||
"wipo" : null
|
|
||||||
},
|
|
||||||
"postalCode" : "22314-1176",
|
|
||||||
"countryCode" : "US",
|
|
||||||
"countryName" : "UNITED STATES"
|
|
||||||
},
|
|
||||||
"attorneyName" : "Thomas J. Moore",
|
|
||||||
"attorneyEmail" : {
|
|
||||||
"authIndicator" : "Y",
|
|
||||||
"addresses" : [ "mail@baconthomas.com" ]
|
|
||||||
},
|
|
||||||
"individualFullName" : "THOMAS J. MOORE",
|
|
||||||
"firmName" : "BACON & THOMAS, PLLC",
|
|
||||||
"correspondantPhone" : "703-683-0500",
|
|
||||||
"correspondantFax" : "703-683-1080",
|
|
||||||
"correspondantEmail" : {
|
|
||||||
"authIndicator" : "Y",
|
|
||||||
"addresses" : [ "mail@baconthomas.com" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"serialNumber" : 88123456,
|
|
||||||
"designSearchList" : [ ],
|
|
||||||
"filingDate" : "2018-09-19",
|
|
||||||
"usRegistrationNumber" : "",
|
|
||||||
"filedAsTeasPlusApp" : true,
|
|
||||||
"currentlyAsTeasPlusApp" : true,
|
|
||||||
"filedAsBaseApp" : false,
|
|
||||||
"currentlyAsBaseApp" : false,
|
|
||||||
"filedAsTeasRfApp" : false,
|
|
||||||
"currentlyAsTeasRfApp" : false,
|
|
||||||
"supplementalRegister" : false,
|
|
||||||
"amendPrincipal" : false,
|
|
||||||
"amendSupplemental" : false,
|
|
||||||
"trademark" : false,
|
|
||||||
"certificationMark" : false,
|
|
||||||
"serviceMark" : true,
|
|
||||||
"collectiveMembershipMark" : false,
|
|
||||||
"collectiveServiceMark" : false,
|
|
||||||
"collectiveTradeMark" : false,
|
|
||||||
"status" : 606,
|
|
||||||
"statusDate" : "2019-10-14",
|
|
||||||
"dateAbandoned" : "2019-10-14",
|
|
||||||
"standardChar" : true,
|
|
||||||
"markDrawingCd" : "4",
|
|
||||||
"colorDrawingCurr" : false,
|
|
||||||
"section2f" : false,
|
|
||||||
"section2fPartial" : false,
|
|
||||||
"others" : false,
|
|
||||||
"publishedPrevRegMark" : false,
|
|
||||||
"clsTotal" : 3,
|
|
||||||
"filedUse" : false,
|
|
||||||
"filedItu" : true,
|
|
||||||
"filed44d" : false,
|
|
||||||
"filed44e" : false,
|
|
||||||
"filed66a" : false,
|
|
||||||
"filedNoBasis" : false,
|
|
||||||
"useCurr" : false,
|
|
||||||
"ituCurr" : true,
|
|
||||||
"sect44dCurr" : false,
|
|
||||||
"sect44eCurr" : false,
|
|
||||||
"sect66aCurr" : false,
|
|
||||||
"noBasisCurr" : false,
|
|
||||||
"useAmended" : false,
|
|
||||||
"ituAmended" : false,
|
|
||||||
"sect44dAmended" : false,
|
|
||||||
"sect44eAmended" : false,
|
|
||||||
"attrnyDktNumber" : "MYLI6005/TJM",
|
|
||||||
"sect8Filed" : false,
|
|
||||||
"sect8Acpt" : false,
|
|
||||||
"sect8PartialAcpt" : false,
|
|
||||||
"sect15Filed" : false,
|
|
||||||
"sect15Ack" : false,
|
|
||||||
"sect71Filed" : false,
|
|
||||||
"sect71Acpt" : false,
|
|
||||||
"sect71PartialAcpt" : false,
|
|
||||||
"renewalFiled" : false,
|
|
||||||
"changeInReg" : false,
|
|
||||||
"lawOffAsgnCd" : "M40",
|
|
||||||
"currLocationCd" : "700",
|
|
||||||
"currLocationDt" : "2019-03-12",
|
|
||||||
"chargeToLocation" : null,
|
|
||||||
"phyLocation" : null,
|
|
||||||
"phyLocationDt" : null,
|
|
||||||
"extStatusDesc" : "Abandoned because no Statement of Use or Extension Request timely filed after Notice of Allowance was issued. To view all documents in this file, click on the Trademark Document Retrieval link at the top of this page. ",
|
|
||||||
"intStatusDesc" : null,
|
|
||||||
"markDrawDesc" : "STANDARD CHARACTER MARK",
|
|
||||||
"currentLoc" : "INTENT TO USE SECTION",
|
|
||||||
"correction" : "",
|
|
||||||
"disclaimer" : "\"UL\"",
|
|
||||||
"markElement" : "MYLIFE UL PROTECT",
|
|
||||||
"parentOf" : [ ],
|
|
||||||
"prevRegNumList" : [ ],
|
|
||||||
"newLawOffAsgnCd" : "113",
|
|
||||||
"lawOffAssigned" : "LAW OFFICE 113",
|
|
||||||
"tm5Status" : 10,
|
|
||||||
"tm5StatusDesc" : "DEAD/APPLICATION/Refused/Dismissed or Invalidated",
|
|
||||||
"tm5StatusDef" : "This trademark application was refused, dismissed, or invalidated by the Office and this application is no longer active.",
|
|
||||||
"physicalLocationHistory" : [ {
|
|
||||||
"eventDate" : "2018-09-25",
|
|
||||||
"physicalLocation" : "MADCD",
|
|
||||||
"physicalLocationDescription" : "NO PHYSICAL FILE"
|
|
||||||
}, {
|
|
||||||
"eventDate" : "2018-09-22",
|
|
||||||
"physicalLocation" : "OUT",
|
|
||||||
"physicalLocationDescription" : "NO PHYSICAL FILE"
|
|
||||||
} ],
|
|
||||||
"pseudoMark" : null
|
|
||||||
},
|
|
||||||
"parties" : {
|
|
||||||
"ownerGroups" : {
|
|
||||||
"20" : [ {
|
|
||||||
"serialNumber" : 88123456,
|
|
||||||
"partyType" : 20,
|
|
||||||
"partyTypeDescription" : "OWNER AT PUBLICATION",
|
|
||||||
"reelFrame" : null,
|
|
||||||
"entityNum" : 1,
|
|
||||||
"entityType" : {
|
|
||||||
"code" : 3,
|
|
||||||
"description" : "CORPORATION"
|
|
||||||
},
|
|
||||||
"name" : "Modern Woodmen of America",
|
|
||||||
"composedOf" : null,
|
|
||||||
"dbaAkaFormerly" : null,
|
|
||||||
"assignment" : null,
|
|
||||||
"address1" : "1701 - 1st Avenue",
|
|
||||||
"address2" : "",
|
|
||||||
"city" : "Rock Island",
|
|
||||||
"addressStateCountry" : {
|
|
||||||
"stateCountry" : {
|
|
||||||
"code" : "IL",
|
|
||||||
"name" : "ILLINOIS"
|
|
||||||
},
|
|
||||||
"isoRegion" : {
|
|
||||||
"code" : "IL",
|
|
||||||
"name" : "ILLINOIS"
|
|
||||||
},
|
|
||||||
"iso" : {
|
|
||||||
"code" : "US",
|
|
||||||
"name" : "UNITED STATES"
|
|
||||||
},
|
|
||||||
"wipo" : null
|
|
||||||
},
|
|
||||||
"zip" : "61201",
|
|
||||||
"citizenship" : {
|
|
||||||
"stateCountry" : {
|
|
||||||
"code" : "IL",
|
|
||||||
"name" : "ILLINOIS"
|
|
||||||
},
|
|
||||||
"isoRegion" : {
|
|
||||||
"code" : "IL",
|
|
||||||
"name" : "ILLINOIS"
|
|
||||||
},
|
|
||||||
"iso" : {
|
|
||||||
"code" : "US",
|
|
||||||
"name" : "UNITED STATES"
|
|
||||||
},
|
|
||||||
"wipo" : null
|
|
||||||
}
|
|
||||||
} ],
|
|
||||||
"10" : [ {
|
|
||||||
"serialNumber" : 88123456,
|
|
||||||
"partyType" : 10,
|
|
||||||
"partyTypeDescription" : "ORIGINAL APPLICANT",
|
|
||||||
"reelFrame" : null,
|
|
||||||
"entityNum" : 1,
|
|
||||||
"entityType" : {
|
|
||||||
"code" : 3,
|
|
||||||
"description" : "CORPORATION"
|
|
||||||
},
|
|
||||||
"name" : "Modern Woodmen of America",
|
|
||||||
"composedOf" : null,
|
|
||||||
"dbaAkaFormerly" : null,
|
|
||||||
"assignment" : null,
|
|
||||||
"address1" : "1701 - 1st Avenue",
|
|
||||||
"address2" : "",
|
|
||||||
"city" : "Rock Island",
|
|
||||||
"addressStateCountry" : {
|
|
||||||
"stateCountry" : {
|
|
||||||
"code" : "IL",
|
|
||||||
"name" : "ILLINOIS"
|
|
||||||
},
|
|
||||||
"isoRegion" : {
|
|
||||||
"code" : "IL",
|
|
||||||
"name" : "ILLINOIS"
|
|
||||||
},
|
|
||||||
"iso" : {
|
|
||||||
"code" : "US",
|
|
||||||
"name" : "UNITED STATES"
|
|
||||||
},
|
|
||||||
"wipo" : null
|
|
||||||
},
|
|
||||||
"zip" : "61201",
|
|
||||||
"citizenship" : {
|
|
||||||
"stateCountry" : {
|
|
||||||
"code" : "IL",
|
|
||||||
"name" : "ILLINOIS"
|
|
||||||
},
|
|
||||||
"isoRegion" : {
|
|
||||||
"code" : "IL",
|
|
||||||
"name" : "ILLINOIS"
|
|
||||||
},
|
|
||||||
"iso" : {
|
|
||||||
"code" : "US",
|
|
||||||
"name" : "UNITED STATES"
|
|
||||||
},
|
|
||||||
"wipo" : null
|
|
||||||
}
|
|
||||||
} ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gsList" : [ {
|
|
||||||
"serialNumber" : 88123456,
|
|
||||||
"internationalClassPrime" : true,
|
|
||||||
"usClasses" : [ {
|
|
||||||
"code" : "100",
|
|
||||||
"description" : "Miscellaneous"
|
|
||||||
}, {
|
|
||||||
"code" : "101",
|
|
||||||
"description" : "Advertising and Business"
|
|
||||||
}, {
|
|
||||||
"code" : "102",
|
|
||||||
"description" : "Insurance and Financial"
|
|
||||||
} ],
|
|
||||||
"internationalClasses" : [ {
|
|
||||||
"code" : "036",
|
|
||||||
"description" : "Insurance and financial"
|
|
||||||
} ],
|
|
||||||
"pseudoClasses" : [ ],
|
|
||||||
"statusCode" : "6",
|
|
||||||
"statusDescription" : "ACTIVE",
|
|
||||||
"statusDate" : "2018-09-25",
|
|
||||||
"firstUseDate" : null,
|
|
||||||
"firstUseInCommerceDate" : null,
|
|
||||||
"firstUseDateDescription" : null,
|
|
||||||
"firstUseInCommerceDateDescription" : null,
|
|
||||||
"description" : "Insurance administration in the field of life insurance; Insurance agencies in the field of life insurance; Insurance agency and brokerage; Insurance brokerage in the field of life insurance; Insurance services, namely, underwriting life insurance; Insurance services, namely, underwriting, issuance and administration of life insurance; Insurance underwriting in the field of life insurance; Issuance of life insurance",
|
|
||||||
"classBasis" : null,
|
|
||||||
"primeClassCode" : "036"
|
|
||||||
} ],
|
|
||||||
"foreignInfoList" : [ ],
|
|
||||||
"prosecutionHistory" : [ {
|
|
||||||
"entryNumber" : 18,
|
|
||||||
"entryCode" : "MAB6",
|
|
||||||
"entryType" : "E",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2019-10-15T04:00:00.000+0000",
|
|
||||||
"entryDesc" : "ABANDONMENT NOTICE E-MAILED - NO USE STATEMENT FILED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 17,
|
|
||||||
"entryCode" : "ABN6",
|
|
||||||
"entryType" : "S",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2019-10-14T04:00:00.000+0000",
|
|
||||||
"entryDesc" : "ABANDONMENT - NO USE STATEMENT FILED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 16,
|
|
||||||
"entryCode" : "NOAM",
|
|
||||||
"entryType" : "E",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2019-03-12T04:00:00.000+0000",
|
|
||||||
"entryDesc" : "NOA E-MAILED - SOU REQUIRED FROM APPLICANT"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 15,
|
|
||||||
"entryCode" : "NPUB",
|
|
||||||
"entryType" : "E",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2019-01-15T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "OFFICIAL GAZETTE PUBLICATION CONFIRMATION E-MAILED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 14,
|
|
||||||
"entryCode" : "PUBO",
|
|
||||||
"entryType" : "A",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2019-01-15T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "PUBLISHED FOR OPPOSITION"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 13,
|
|
||||||
"entryCode" : "NONP",
|
|
||||||
"entryType" : "E",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-12-26T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "NOTIFICATION OF NOTICE OF PUBLICATION E-MAILED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 12,
|
|
||||||
"entryCode" : "ALIE",
|
|
||||||
"entryType" : "A",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-12-06T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "ASSIGNED TO LIE"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 11,
|
|
||||||
"entryCode" : "CNSA",
|
|
||||||
"entryType" : "P",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-11-15T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "APPROVED FOR PUB - PRINCIPAL REGISTER"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 10,
|
|
||||||
"entryCode" : "XAEC",
|
|
||||||
"entryType" : "I",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-11-09T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "EXAMINER'S AMENDMENT ENTERED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 9,
|
|
||||||
"entryCode" : "GNEN",
|
|
||||||
"entryType" : "O",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-11-09T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "NOTIFICATION OF EXAMINERS AMENDMENT E-MAILED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 8,
|
|
||||||
"entryCode" : "GNEA",
|
|
||||||
"entryType" : "O",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-11-09T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "EXAMINERS AMENDMENT E-MAILED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 7,
|
|
||||||
"entryCode" : "CNEA",
|
|
||||||
"entryType" : "R",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-11-09T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "EXAMINERS AMENDMENT -WRITTEN"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 6,
|
|
||||||
"entryCode" : "GNRN",
|
|
||||||
"entryType" : "O",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-11-08T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "NOTIFICATION OF NON-FINAL ACTION E-MAILED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 5,
|
|
||||||
"entryCode" : "GNRT",
|
|
||||||
"entryType" : "F",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-11-08T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "NON-FINAL ACTION E-MAILED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 4,
|
|
||||||
"entryCode" : "CNRT",
|
|
||||||
"entryType" : "R",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-11-08T05:00:00.000+0000",
|
|
||||||
"entryDesc" : "NON-FINAL ACTION WRITTEN"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 3,
|
|
||||||
"entryCode" : "DOCK",
|
|
||||||
"entryType" : "D",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-10-31T04:00:00.000+0000",
|
|
||||||
"entryDesc" : "ASSIGNED TO EXAMINER"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 2,
|
|
||||||
"entryCode" : "NWOS",
|
|
||||||
"entryType" : "I",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-09-25T04:00:00.000+0000",
|
|
||||||
"entryDesc" : "NEW APPLICATION OFFICE SUPPLIED DATA ENTERED"
|
|
||||||
}, {
|
|
||||||
"entryNumber" : 1,
|
|
||||||
"entryCode" : "NWAP",
|
|
||||||
"entryType" : "I",
|
|
||||||
"proceedingNum" : 0,
|
|
||||||
"entryDate" : "2018-09-22T04:00:00.000+0000",
|
|
||||||
"entryDesc" : "NEW APPLICATION ENTERED"
|
|
||||||
} ],
|
|
||||||
"relationshipBundleList" : [ ],
|
|
||||||
"internationalData" : false,
|
|
||||||
"publication" : {
|
|
||||||
"serialNumber" : 88123456,
|
|
||||||
"datePublished" : "2019-01-15",
|
|
||||||
"noticeOfAllowanceDate" : "2019-03-12",
|
|
||||||
"officialGazettes" : [ ]
|
|
||||||
},
|
|
||||||
"divisional" : {
|
|
||||||
"serialNumber" : 88123456,
|
|
||||||
"childOf" : null,
|
|
||||||
"parentOfList" : [ ]
|
|
||||||
}
|
|
||||||
} ]
|
|
||||||
}
|
|
||||||