From 02858146b3972061804aeb8348cd368a33164513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=A0=E5=B1=B1?= <17738440858@163.com> Date: Fri, 28 Nov 2025 17:14:00 +0800 Subject: [PATCH] style(components): format CSS styles in Vue components - Remove extra spaces in CSS property declarations - Consolidate multi-line CSS rules into single lines - Maintain consistent formatting across component styles - Improve readability by removing unnecessary line breaks - Ensure uniform styling structure in scoped CSS blocks --- CLAUDE.md | 270 ++++++ electron-vue-template/src/main/main.ts | 11 +- electron-vue-template/src/renderer/App.vue | 514 ++-------- .../src/renderer/api/http.ts | 19 +- .../components/amazon/AmazonDashboard.vue | 874 ++++-------------- .../components/amazon/AsinQueryPanel.vue | 68 +- .../components/amazon/GenmaiSpiritPanel.vue | 84 +- .../components/amazon/TrademarkCheckPanel.vue | 556 ++--------- .../renderer/components/auth/LoginDialog.vue | 38 +- .../components/auth/RegisterDialog.vue | 38 +- .../components/common/AccountManager.vue | 77 +- .../components/common/SettingsDialog.vue | 787 +++------------- .../components/common/TrialExpiredDialog.vue | 129 +-- .../components/common/UpdateDialog.vue | 334 +------ .../components/layout/NavigationBar.vue | 247 +---- .../components/rakuten/RakutenDashboard.vue | 302 ++---- .../components/zebra/ZebraDashboard.vue | 169 ++-- .../src/renderer/config/index.ts | 20 + .../erp/common/Alibaba1688Constants.java | 21 + .../tashow/erp/common/AmazonConstants.java | 36 + .../com/tashow/erp/common/BanmaConstants.java | 23 + .../com/tashow/erp/common/CacheConstants.java | 13 + .../java/com/tashow/erp/common/Constants.java | 169 +--- .../tashow/erp/common/FangzhouConstants.java | 14 + .../com/tashow/erp/common/HttpConstants.java | 13 + .../tashow/erp/common/RakutenConstants.java | 16 + .../erp/controller/RakutenController.java | 5 +- .../service/impl/Alibaba1688ServiceImpl.java | 4 + .../impl/AmazonScrapingServiceImpl.java | 97 +- .../erp/service/impl/AuthServiceImpl.java | 10 + .../service/impl/BanmaOrderServiceImpl.java | 165 ++-- .../impl/BrandTrademarkCacheServiceImpl.java | 22 +- .../service/impl/FangzhouApiServiceImpl.java | 55 +- .../erp/service/impl/GenmaiServiceImpl.java | 13 + .../service/impl/RakutenCacheServiceImpl.java | 25 +- .../impl/RakutenScrapingServiceImpl.java | 6 + .../com/tashow/erp/utils/DataReportUtil.java | 3 +- .../com/tashow/erp/utils/ErrorReporter.java | 15 +- .../com/tashow/erp/utils/StringUtils.java | 30 +- .../java/com/tashow/erp/utils/UrlBuilder.java | 28 + .../com/tashow/erp/utils/ValidationUtils.java | 22 + .../src/main/resources/application.yml | 4 +- .../monitor/ClientAccountController.java | 3 - .../controller/monitor/VersionController.java | 103 ++- .../impl/ClientMonitorServiceImpl.java | 45 +- ruoyi-ui/src/api/monitor/version.js | 2 +- ruoyi-ui/src/views/monitor/account/index.vue | 1 + ruoyi-ui/src/views/monitor/version/index.vue | 74 +- 48 files changed, 1746 insertions(+), 3828 deletions(-) create mode 100644 CLAUDE.md create mode 100644 electron-vue-template/src/renderer/config/index.ts create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/common/Alibaba1688Constants.java create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/common/AmazonConstants.java create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/common/BanmaConstants.java create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/common/CacheConstants.java create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/common/FangzhouConstants.java create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/common/HttpConstants.java create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/common/RakutenConstants.java create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/utils/UrlBuilder.java create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/utils/ValidationUtils.java diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e1eaaeb --- /dev/null +++ b/CLAUDE.md @@ -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 ``, ``, ``, and `` 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('/your-endpoint', data) + } + } + ``` + +3. **Component usage**: + ```vue + + ``` + +### 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 ` \ No newline at end of file diff --git a/electron-vue-template/src/renderer/components/layout/NavigationBar.vue b/electron-vue-template/src/renderer/components/layout/NavigationBar.vue index adc867e..318aa73 100644 --- a/electron-vue-template/src/renderer/components/layout/NavigationBar.vue +++ b/electron-vue-template/src/renderer/components/layout/NavigationBar.vue @@ -140,227 +140,44 @@ onMounted(async () => { \ No newline at end of file diff --git a/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue b/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue index 497ab58..d667a86 100644 --- a/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue +++ b/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue @@ -649,229 +649,101 @@ onMounted(loadLatest) diff --git a/electron-vue-template/src/renderer/config/index.ts b/electron-vue-template/src/renderer/config/index.ts new file mode 100644 index 0000000..f2fcba4 --- /dev/null +++ b/electron-vue-template/src/renderer/config/index.ts @@ -0,0 +1,20 @@ +/** + * 应用配置 + */ +export const AppConfig = { + CLIENT_BASE: 'http://localhost:8081', + RUOYI_BASE: 'http://192.168.1.89: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') +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/Alibaba1688Constants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/Alibaba1688Constants.java new file mode 100644 index 0000000..2491eaf --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/Alibaba1688Constants.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/AmazonConstants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/AmazonConstants.java new file mode 100644 index 0000000..8d958dd --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/AmazonConstants.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/BanmaConstants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/BanmaConstants.java new file mode 100644 index 0000000..239e146 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/BanmaConstants.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/CacheConstants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/CacheConstants.java new file mode 100644 index 0000000..653bf90 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/CacheConstants.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/Constants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/Constants.java index dd6f255..417de9d 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/common/Constants.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/Constants.java @@ -1,169 +1,16 @@ package com.tashow.erp.common; - import java.util.Locale; /** - * 通用常量信息 - * - * @author ruoyi + * 通用常量(保留兼容性) + * 新代码请使用具体业务常量类:AmazonConstants、RakutenConstants、HttpConstants等 */ -public class Constants -{ - /** - * UTF-8 字符集 - */ - public static final String UTF8 = "UTF-8"; +@Deprecated +public class Constants { + public static final String HTTP = HttpConstants.HTTP; + public static final String HTTPS = HttpConstants.HTTPS; + public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE; - /** - * 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" }; + private Constants() {} } diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/FangzhouConstants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/FangzhouConstants.java new file mode 100644 index 0000000..7720a00 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/FangzhouConstants.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/HttpConstants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/HttpConstants.java new file mode 100644 index 0000000..2b9c32c --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/HttpConstants.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/common/RakutenConstants.java b/erp_client_sb/src/main/java/com/tashow/erp/common/RakutenConstants.java new file mode 100644 index 0000000..8200010 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/common/RakutenConstants.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/controller/RakutenController.java b/erp_client_sb/src/main/java/com/tashow/erp/controller/RakutenController.java index 80f760b..03c3886 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/controller/RakutenController.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/controller/RakutenController.java @@ -1,4 +1,6 @@ package com.tashow.erp.controller; + +import com.tashow.erp.common.RakutenConstants; import com.tashow.erp.model.RakutenProduct; import com.tashow.erp.model.SearchResult; 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.multipart.MultipartFile; import jakarta.servlet.http.HttpServletRequest; + import java.util.*; /** @@ -81,7 +84,7 @@ public class RakutenController { } int cachedCount = allProducts.size() - newProducts.size(); if (cachedCount > 0) { - dataReportUtil.reportDataCollection("RAKUTEN_CACHE", cachedCount, "0"); + dataReportUtil.reportDataCollection(RakutenConstants.DATA_TYPE_CACHE, cachedCount, "0"); } return JsonData.buildSuccess(Map.of( "products", allProducts, diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java index db484af..450da91 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/Alibaba1688ServiceImpl.java @@ -39,6 +39,10 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service { private final RestTemplate noSslRestTemplate = createNoSslRestTemplate(); @Autowired private ErrorReporter errorReporter; + + /** + * 创建忽略SSL证书的RestTemplate + */ private RestTemplate createNoSslRestTemplate() { try { TrustManager[] trustManagers = new TrustManager[] { diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java index 3b27f06..8368fc8 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java @@ -1,10 +1,15 @@ 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.repository.AmazonProductRepository; import com.tashow.erp.service.AmazonScrapingService; import com.tashow.erp.utils.DataReportUtil; import com.tashow.erp.utils.ErrorReporter; 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.stereotype.Service; import us.codecraft.webmagic.Page; @@ -12,13 +17,13 @@ import us.codecraft.webmagic.Site; import us.codecraft.webmagic.Spider; import us.codecraft.webmagic.processor.PageProcessor; import us.codecraft.webmagic.selector.Html; + import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** - * 亚马逊数据采集服务实现类 - * - * @author ruoyi + * 亚马逊数据采集服务实现 + * 负责批量采集亚马逊商品信息并缓存 */ @Service public class AmazonScrapingServiceImpl implements AmazonScrapingService, PageProcessor { @@ -42,7 +47,7 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro String url = page.getUrl().toString(); // 提取ASIN String asin = html.xpath("//input[@id='ASIN']/@value").toString(); - if (isEmpty(asin)) { + if (ValidationUtils.isEmpty(asin)) { String[] parts = url.split("/dp/"); 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 priceWhole = html.xpath("//span[@class='a-price-whole']/text()").toString(); String price = null; - if (!isEmpty(priceSymbol) && !isEmpty(priceWhole)) { + if (ValidationUtils.isNotEmpty(priceSymbol) && ValidationUtils.isNotEmpty(priceWhole)) { price = priceSymbol + priceWhole; } - if (isEmpty(price)) { + if (ValidationUtils.isEmpty(price)) { price = html.xpath("//span[@class='a-price-range']/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(); } // 关键数据为空时重试 - if (isEmpty(price) && isEmpty(seller)) { + if (ValidationUtils.isEmpty(price) && ValidationUtils.isEmpty(seller)) { throw new RuntimeException("Retry this page"); } - if (isEmpty(price)) errorReporter.reportDataEmpty("amazon", asin, price); - if (isEmpty(seller)) errorReporter.reportDataEmpty("amazon", asin, seller); + if (ValidationUtils.isEmpty(price)) errorReporter.reportDataEmpty(AmazonConstants.DATA_TYPE.toLowerCase(), asin, price); + if (ValidationUtils.isEmpty(seller)) errorReporter.reportDataEmpty(AmazonConstants.DATA_TYPE.toLowerCase(), asin, seller); AmazonProductEntity entity = new AmazonProductEntity(); entity.setAsin(asin == null ? "" : asin); + entity.setPrice(price); entity.setSeller(seller); resultCache.put(asin, entity); page.putField("entity", entity); } - /** * 获取WebMagic站点配置 */ @@ -86,31 +91,30 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro } /** - * 批量获取产品信息 + * 批量获取亚马逊商品信息 + * 优先从缓存读取,缓存未命中时实时采集 */ @Override public List batchGetProductInfo(List asinList, String batchId, String region) { - String sessionId = (batchId != null) ? batchId : "SINGLE_" + UUID.randomUUID(); - LocalDateTime batchTime = LocalDateTime.now(); - + String sessionId = (batchId != null) ? batchId : AmazonConstants.SESSION_PREFIX + UUID.randomUUID(); resultCache.clear(); - // 第一步:清理1小时前的所有旧数据 - amazonProductRepository.deleteAllDataBefore(LocalDateTime.now().minusHours(1)); + // 清理过期缓存数据 + amazonProductRepository.deleteAllDataBefore(LocalDateTime.now().minusHours(CacheConstants.DATA_RETENTION_HOURS)); - // 优化:批次内复用代理检测和工具实例 + // 批次内复用代理检测和下载器实例 RakutenProxyUtil proxyUtil = new RakutenProxyUtil(); - String sampleUrl = buildAmazonUrl(region, "B00000000"); + String sampleUrl = UrlBuilder.buildAmazonUrl(region, "B00000000"); var proxyDownloader = proxyUtil.createProxyDownloader(proxyUtil.detectSystemProxy(sampleUrl)); - // 第二步:处理每个ASIN + // 处理每个ASIN Map allProducts = new HashMap<>(); for (String asin : asinList.stream().distinct().toList()) { if (asin == null || asin.trim().isEmpty()) continue; String cleanAsin = asin.replaceAll("[^a-zA-Z0-9]", ""); Optional 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 entity = new AmazonProductEntity(); entity.setAsin(cachedEntity.getAsin()); @@ -122,7 +126,7 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro amazonProductRepository.save(entity); allProducts.put(cleanAsin, entity); } else { - String url = buildAmazonUrl(region, cleanAsin); + String url = UrlBuilder.buildAmazonUrl(region, cleanAsin); this.site = configureSiteForRegion(region); synchronized (spiderLock) { activeSpider = Spider.create(this).addUrl(url).setDownloader(proxyDownloader).thread(1); @@ -135,51 +139,38 @@ public class AmazonScrapingServiceImpl implements AmazonScrapingService, PagePro entity.setSessionId(sessionId); entity.setUpdatedAt(LocalDateTime.now()); amazonProductRepository.save(entity); - dataReportUtil.reportDataCollection("AMAZON", 1, "0"); + dataReportUtil.reportDataCollection(AmazonConstants.DATA_TYPE, 1, "0"); allProducts.put(cleanAsin, entity); } } return new ArrayList<>(allProducts.values()); } - private boolean isEmpty(String str) { - return str == null || str.trim().isEmpty(); - } - /** - * 根据地区构建Amazon URL - */ - 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 + * 根据地区配置WebMagic站点 */ private Site configureSiteForRegion(String region) { - boolean isUS = "US".equals(region); + boolean isUS = AmazonConstants.REGION_US.equals(region); return Site.me() - .setRetryTimes(3) - .setSleepTime(2000 + random.nextInt(2000)) - .setTimeOut(20000) - .setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36") - .addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8") - .addHeader("accept-encoding", "gzip, deflate, br") - .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("cache-control", "max-age=0") - .addHeader("referer", isUS ? "https://www.amazon.com/" : "https://www.amazon.co.jp/") + .setRetryTimes(AmazonConstants.RETRY_TIMES) + .setSleepTime(AmazonConstants.SLEEP_TIME_BASE + random.nextInt(AmazonConstants.SLEEP_TIME_RANDOM)) + .setTimeOut(AmazonConstants.TIMEOUT_MS) + .setUserAgent(AmazonConstants.USER_AGENT) + .addHeader("accept", AmazonConstants.HEADER_ACCEPT) + .addHeader("accept-encoding", AmazonConstants.HEADER_ACCEPT_ENCODING) + .addHeader("accept-language", isUS ? AmazonConstants.HEADER_ACCEPT_LANGUAGE_US : AmazonConstants.HEADER_ACCEPT_LANGUAGE_JP) + .addHeader("cache-control", AmazonConstants.HEADER_CACHE_CONTROL) + .addHeader("referer", isUS ? AmazonConstants.DOMAIN_US + "/" : AmazonConstants.DOMAIN_JP + "/") .addHeader("sec-fetch-site", "none") .addHeader("sec-fetch-mode", "navigate") .addHeader("sec-fetch-user", "?1") .addHeader("sec-fetch-dest", "document") - .addCookie("i18n-prefs", isUS ? "USD" : "JPY") - .addCookie(isUS ? "lc-main" : "lc-acbjp", isUS ? "en_US" : "zh_CN") - .addCookie("session-id", isUS ? "134-6097934-2082600" : "358-1261309-0483141") - .addCookie("session-id-time", "2082787201l") - .addCookie(isUS ? "ubid-main" : "ubid-acbjp", isUS ? "132-7547587-3056927" : "357-8224002-9668932") - .addCookie("skin", "noskin"); + .addCookie("i18n-prefs", isUS ? AmazonConstants.COOKIE_I18N_PREFS_US : AmazonConstants.COOKIE_I18N_PREFS_JP) + .addCookie(isUS ? "lc-main" : "lc-acbjp", isUS ? AmazonConstants.COOKIE_LC_US : AmazonConstants.COOKIE_LC_JP) + .addCookie("session-id", isUS ? AmazonConstants.COOKIE_SESSION_ID_US : AmazonConstants.COOKIE_SESSION_ID_JP) + .addCookie("session-id-time", AmazonConstants.COOKIE_SESSION_ID_TIME) + .addCookie(isUS ? "ubid-main" : "ubid-acbjp", isUS ? AmazonConstants.COOKIE_UBID_US : AmazonConstants.COOKIE_UBID_JP) + .addCookie("skin", AmazonConstants.COOKIE_SKIN); } } \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AuthServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AuthServiceImpl.java index ac6f6f3..8702697 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AuthServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AuthServiceImpl.java @@ -10,6 +10,10 @@ import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; +/** + * 认证服务实现 + * 负责客户端信息管理和错误上报 + */ @Service public class AuthServiceImpl { @@ -22,6 +26,9 @@ public class AuthServiceImpl { @Getter private String clientId = DeviceUtils.generateDeviceId(); + /** + * 获取客户端信息 + */ public Map getClientInfo() { Map info = new HashMap<>(); info.put("clientId", clientId); @@ -30,6 +37,9 @@ public class AuthServiceImpl { return info; } + /** + * 上报错误信息 + */ public void reportError(String errorType, String errorMessage, Exception e) { try { Map errorData = new HashMap<>(); diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BanmaOrderServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BanmaOrderServiceImpl.java index dbc9795..3d57fa0 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BanmaOrderServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BanmaOrderServiceImpl.java @@ -1,5 +1,7 @@ package com.tashow.erp.service.impl; + import com.fasterxml.jackson.databind.ObjectMapper; +import com.tashow.erp.common.BanmaConstants; import com.tashow.erp.entity.BanmaOrderEntity; import com.tashow.erp.repository.BanmaOrderRepository; import com.tashow.erp.service.BanmaOrderService; @@ -15,6 +17,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; + import java.time.Duration; import java.time.LocalDateTime; import java.util.*; @@ -25,13 +28,15 @@ import java.util.stream.Collectors; * * @author ruoyi */ + +/** + * 斑马订单服务实现 + * 负责订单采集、物流查询和数据管理 + */ @Service public class BanmaOrderServiceImpl implements BanmaOrderService { 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}") private String ruoyiAdminBase; private RestTemplate restTemplate; @@ -46,25 +51,30 @@ public class BanmaOrderServiceImpl implements BanmaOrderService { private String currentBatchSessionId = null; // 物流信息缓存,避免重复查询 private final Map trackingInfoCache = new ConcurrentHashMap<>(); + public BanmaOrderServiceImpl(BanmaOrderRepository banmaOrderRepository, CacheService cacheService, DataReportUtil dataReportUtil, ErrorReporter errorReporter) { this.banmaOrderRepository = banmaOrderRepository; this.cacheService = cacheService; this.dataReportUtil = dataReportUtil; this.errorReporter = errorReporter; RestTemplateBuilder builder = new RestTemplateBuilder(); - builder.connectTimeout(Duration.ofSeconds(5)); - builder.readTimeout(Duration.ofSeconds(10)); + builder.connectTimeout(Duration.ofSeconds(BanmaConstants.CONNECT_TIMEOUT_SECONDS)); + builder.readTimeout(Duration.ofSeconds(BanmaConstants.READ_TIMEOUT_SECONDS)); restTemplate = builder.build(); } + + /** + * 从服务器获取认证Token + */ @SuppressWarnings("unchecked") private void fetchTokenFromServer(Long accountId) { Map resp = restTemplate.getForObject(ruoyiAdminBase + "/tool/banma/accounts", Map.class); List> list = (List>) resp.get("data"); if (list == null || list.isEmpty()) return; Map account = accountId != 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 -> 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)); + if (account != null) { String token = (String) account.get("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); HttpEntity 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 response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class); return response.getBody() != null ? response.getBody() : new HashMap<>(); @@ -105,16 +115,16 @@ public class BanmaOrderServiceImpl implements BanmaOrderService { String shopIdsParam = ""; if (shopIds != null && !shopIds.isEmpty()) { List validShopIds = shopIds.stream() - .filter(id -> id != null && !id.trim().isEmpty()) - .collect(Collectors.toList()); + .filter(id -> id != null && !id.trim().isEmpty()) + .collect(Collectors.toList()); if (!validShopIds.isEmpty()) { shopIdsParam = "shopIds[]=" + String.join("&shopIds[]=", validShopIds) + "&"; } } - - String url = (StringUtils.isEmpty(startDate) || StringUtils.isEmpty(endDate)) - ? String.format(API_URL, shopIdsParam, page, pageSize, System.currentTimeMillis()) - : String.format(API_URL_WITH_TIME, shopIdsParam, page, pageSize, startDate, endDate, System.currentTimeMillis()); + + String url = (StringUtils.isEmpty(startDate) || StringUtils.isEmpty(endDate)) + ? String.format(BanmaConstants.API_ORDER_LIST, shopIdsParam, page, pageSize, System.currentTimeMillis()) + : String.format(BanmaConstants.API_ORDER_LIST_WITH_TIME, shopIdsParam, page, pageSize, startDate, endDate, System.currentTimeMillis()); ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class); if (response.getBody() == null || !Integer.valueOf(0).equals(response.getBody().get("code"))) { Map errorResult = new HashMap<>(); @@ -127,14 +137,14 @@ public class BanmaOrderServiceImpl implements BanmaOrderService { int total = ((Number) dataMap.getOrDefault("total", 0)).intValue(); List orders = Optional.ofNullable(dataMap.get("list")) - .map(list -> (List>) list) - .orElse(Collections.emptyList()) - .stream() - .map(this::processOrderData) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .map(list -> (List>) list) + .orElse(Collections.emptyList()) + .stream() + .map(this::processOrderData) + .filter(Objects::nonNull) + .collect(Collectors.toList()); - if (!orders.isEmpty()) dataReportUtil.reportDataCollection("BANMA", orders.size(), "0"); + if (!orders.isEmpty()) dataReportUtil.reportDataCollection(BanmaConstants.DATA_TYPE, orders.size(), "0"); Map result = new HashMap<>(); result.put("orders", orders); @@ -152,89 +162,92 @@ public class BanmaOrderServiceImpl implements BanmaOrderService { */ private Map processOrderData(Map order) { String trackingNumber = (String) order.get("internationalTrackingNumber"); + + // 检查缓存 if (StringUtils.isNotEmpty(trackingNumber)) { - LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1); + LocalDateTime cutoffTime = LocalDateTime.now().minusHours(BanmaConstants.CACHE_HOURS); if (banmaOrderRepository.existsByTrackingNumberAndCreatedAtAfter(trackingNumber, cutoffTime)) { return banmaOrderRepository.findLatestByTrackingNumber(trackingNumber) - .map(entity -> { - if (currentBatchSessionId != null && !currentBatchSessionId.equals(entity.getSessionId())) { - entity.setSessionId(currentBatchSessionId); - entity.setCreatedAt(LocalDateTime.now()); - entity.setUpdatedAt(LocalDateTime.now()); - banmaOrderRepository.save(entity); - } - try { - return objectMapper.readValue(entity.getOrderData(), Map.class); - } catch (Exception e) { - return new HashMap<>(); - } - }) - .orElse(null); + .map(entity -> { + if (currentBatchSessionId != null && !currentBatchSessionId.equals(entity.getSessionId())) { + entity.setSessionId(currentBatchSessionId); + entity.setUpdatedAt(LocalDateTime.now()); + banmaOrderRepository.save(entity); + } + try { + return objectMapper.readValue(entity.getOrderData(), Map.class); + } catch (Exception e) { + logger.warn("解析缓存订单数据失败: {}", trackingNumber); + return new HashMap<>(); + } + }) + .orElse(null); } else { - banmaOrderRepository.findByTrackingNumber(trackingNumber) - .ifPresent(banmaOrderRepository::delete); + banmaOrderRepository.findByTrackingNumber(trackingNumber).ifPresent(banmaOrderRepository::delete); } } - // 构建新订单数据 + // 构建新订单 Map result = new HashMap<>(); result.put("internationalTrackingNumber", trackingNumber); result.put("internationalShippingFee", order.get("internationalShippingFee")); result.put("trackInfo", trackingInfoCache.computeIfAbsent(trackingNumber, this::fetchTrackingInfo)); Optional.ofNullable(order.get("subOrders")) - .map(sub -> (List>) sub) - .filter(list -> !list.isEmpty()) - .map(list -> list.get(0)) - .ifPresent(subOrder -> extractSubOrderFields(result, subOrder)); + .map(sub -> (List>) sub) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(0)) + .ifPresent(subOrder -> extractSubOrderFields(result, subOrder)); - BanmaOrderEntity entity = new BanmaOrderEntity(); String entityTrackingNumber = (String) result.get("internationalTrackingNumber"); String shopOrderNumber = (String) result.get("shopOrderNumber"); 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(shopOrderNumber)) errorReporter.reportDataEmpty("banma", String.valueOf(result.get("id")), shopOrderNumber); - if (StringUtils.isEmpty(productTitle)) errorReporter.reportDataEmpty("banma", String.valueOf(result.get("id")), productTitle); + // 上报空数据 + if (StringUtils.isEmpty(entityTrackingNumber)) + errorReporter.reportDataEmpty(BanmaConstants.DATA_TYPE.toLowerCase(), orderId, entityTrackingNumber); + 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.isNotEmpty(shopOrderNumber)) { - entityTrackingNumber = "ORDER_" + shopOrderNumber; + entityTrackingNumber = BanmaConstants.TRACKING_PREFIX_ORDER + shopOrderNumber; } else if (StringUtils.isNotEmpty(productTitle)) { - entityTrackingNumber = "PRODUCT_" + Math.abs(productTitle.hashCode()); + entityTrackingNumber = BanmaConstants.TRACKING_PREFIX_PRODUCT + Math.abs(productTitle.hashCode()); } 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 String sessionId = currentBatchSessionId != null ? currentBatchSessionId : - Optional.ofNullable((String) result.get("orderedAt")) - .filter(orderedAt -> orderedAt.length() >= 10) - .map(orderedAt -> "SESSION_" + orderedAt.substring(0, 10)) - .orElse("SESSION_" + java.time.LocalDate.now().toString()); + Optional.ofNullable((String) result.get("orderedAt")) + .filter(orderedAt -> orderedAt.length() >= 10) + .map(orderedAt -> BanmaConstants.SESSION_PREFIX + orderedAt.substring(0, 10)) + .orElse(BanmaConstants.SESSION_PREFIX + java.time.LocalDate.now()); + // 保存实体 + BanmaOrderEntity entity = new BanmaOrderEntity(); + entity.setTrackingNumber(entityTrackingNumber); entity.setSessionId(sessionId); entity.setCreatedAt(LocalDateTime.now()); entity.setUpdatedAt(LocalDateTime.now()); - try { + entity.setOrderData(objectMapper.writeValueAsString(result)); banmaOrderRepository.save(entity); } catch (Exception e) { - logger.warn("保存订单数据失败,跳过: {}", entityTrackingNumber); + logger.warn("保存订单失败: {}", entityTrackingNumber, e); } - return result; } + /** + * 提取子订单字段 + */ private void extractSubOrderFields(Map simplifiedOrder, Map subOrder) { String[] basicFields = {"orderedAt", "timeSinceOrder", "createdAt", "poTrackingNumber"}; 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))); } + + /** + * 获取物流信息 + */ private String fetchTrackingInfo(String trackingNumber) { Map trackInfoMap = (Map) new SagawaExpressSdk().getTrackingInfo(trackingNumber).get("trackInfo"); if (trackInfoMap != null) { return trackInfoMap.get("dateTime") + " " + trackInfoMap.get("office") + " " + trackInfoMap.get("status"); } - ResponseEntity response = restTemplate.getForEntity(String.format(TRACKING_URL, trackingNumber), Map.class); + ResponseEntity response = restTemplate.getForEntity(String.format(BanmaConstants.API_TRACKING, trackingNumber), Map.class); return Optional.ofNullable(response.getBody()) - .filter(body -> Integer.valueOf(0).equals(body.get("code"))) - .map(body -> (List>) body.get("data")) - .filter(list -> !list.isEmpty()) - .map(list -> list.get(0)) - .map(track -> (String) track.get("track")) - .orElse("暂无物流信息"); + .filter(body -> Integer.valueOf(0).equals(body.get("code"))) + .map(body -> (List>) body.get("data")) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(0)) + .map(track -> (String) track.get("track")) + .orElse("暂无物流信息"); } } diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BrandTrademarkCacheServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BrandTrademarkCacheServiceImpl.java index 624c329..7e5a306 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BrandTrademarkCacheServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/BrandTrademarkCacheServiceImpl.java @@ -1,5 +1,6 @@ package com.tashow.erp.service.impl; +import com.tashow.erp.common.CacheConstants; import com.tashow.erp.entity.BrandTrademarkCacheEntity; import com.tashow.erp.repository.BrandTrademarkCacheRepository; import com.tashow.erp.service.BrandTrademarkCacheService; @@ -7,30 +8,40 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; +/** + * 品牌商标缓存服务实现 + * 提供商标查询结果的缓存管理 + */ @Slf4j @Service public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheService { - @Autowired private BrandTrademarkCacheRepository repository; + /** + * 从缓存获取品牌商标注册状态 + */ @Override public Map getCached(List brands) { - LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1); + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(CacheConstants.TRADEMARK_CACHE_DAYS); List cached = repository.findByBrandInAndCreatedAtAfter(brands, cutoffTime); Map result = new HashMap<>(); cached.forEach(e -> result.put(e.getBrand(), e.getRegistered())); return result; } + /** + * 保存商标查询结果到缓存 + */ @Override public void saveResults(Map results) { - LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1); + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(CacheConstants.TRADEMARK_CACHE_DAYS); results.forEach((brand, registered) -> { repository.findByBrandAndCreatedAtAfter(brand, cutoffTime) .ifPresentOrElse( @@ -49,10 +60,13 @@ public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheServic }); } + /** + * 清理过期缓存数据 + */ @Override @Transactional public void cleanExpired() { - LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1); + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(CacheConstants.TRADEMARK_CACHE_DAYS); repository.deleteByCreatedAtBefore(cutoffTime); } } diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/FangzhouApiServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/FangzhouApiServiceImpl.java index c9afaa8..5c98808 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/FangzhouApiServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/FangzhouApiServiceImpl.java @@ -2,6 +2,7 @@ package com.tashow.erp.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.tashow.erp.common.FangzhouConstants; import com.tashow.erp.service.IFangzhouApiService; import com.tashow.erp.utils.ApiForwarder; import com.tashow.erp.utils.LoggerUtil; @@ -23,14 +24,12 @@ import java.security.MessageDigest; import java.util.Map; /** - * 方舟精选 API 服务实现 + * 方舟精选API服务实现 + * 负责与方舟精选平台的API交互 */ @Service public class FangzhouApiServiceImpl implements IFangzhouApiService { 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 private RestTemplate restTemplate; @@ -41,6 +40,9 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService { @Autowired private ApiForwarder apiForwarder; + /** + * 获取API Token + */ @Override public String getToken() { try { @@ -60,6 +62,9 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService { } } + /** + * 刷新API Token + */ @Override public String refreshToken() { try { @@ -78,6 +83,9 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService { } } + /** + * 调用方舟精选API + */ @Override public JsonNode callApi(String command, String data, String token) { try { @@ -87,26 +95,26 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService { formData.add("c", command); formData.add("d", data); 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("website", "1"); + formData.add("website", FangzhouConstants.WEBSITE_CODE); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntity> 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); // 处理 Token 失效 int statusCode = json.get("S").asInt(); - if (statusCode == TOKEN_EXPIRED_CODE || statusCode == -1002) { + if (statusCode == FangzhouConstants.TOKEN_EXPIRED_CODE || statusCode == -1002) { String newToken = statusCode == -1002 ? getToken() : refreshToken(); logger.info("Token 失效({}), {}后重试", statusCode, statusCode == -1002 ? "重新注册" : "刷新"); 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); - result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class); + result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class); json = objectMapper.readTree(result); } @@ -119,14 +127,14 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService { formData.add("c", command); formData.add("d", data); formData.add("t", newToken); - 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("website", "1"); + formData.add("website", FangzhouConstants.WEBSITE_CODE); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntity> requestEntity = new HttpEntity<>(formData, headers); try { - String result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class); + String result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class); return objectMapper.readTree(result); } catch (Exception ex) { logger.error("重试失败", ex); @@ -138,6 +146,9 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService { } } + /** + * 上传文件到方舟精选 + */ @Override public JsonNode uploadFile(MultipartFile file, String token) { try { @@ -149,26 +160,26 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService { formData.add("t", token); formData.add("ts", ts); formData.add("d", data); - formData.add("s", md5(ts + data + API_SECRET)); - formData.add("website", "1"); + 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> 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); // 处理 Token 失效 int statusCode = json.get("S").asInt(); - if (statusCode == TOKEN_EXPIRED_CODE || statusCode == -1002) { + if (statusCode == FangzhouConstants.TOKEN_EXPIRED_CODE || statusCode == -1002) { String newToken = statusCode == -1002 ? getToken() : refreshToken(); logger.info("Token 失效({}), {}后重试", statusCode, statusCode == -1002 ? "重新注册" : "刷新"); 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); - result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class); + result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class); json = objectMapper.readTree(result); } @@ -183,14 +194,14 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService { formData.add("t", newToken); formData.add("ts", ts); formData.add("d", data); - formData.add("s", md5(ts + data + API_SECRET)); - formData.add("website", "1"); + 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> requestEntity = new HttpEntity<>(formData, headers); try { - String result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class); + String result = restTemplate.postForObject(FangzhouConstants.API_URL, requestEntity, String.class); return objectMapper.readTree(result); } catch (Exception ex) { logger.error("重试失败", ex); diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/GenmaiServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/GenmaiServiceImpl.java index 74a1c96..e922ed8 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/GenmaiServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/GenmaiServiceImpl.java @@ -14,6 +14,10 @@ import java.net.URL; import java.util.List; import java.util.Map; +/** + * 跟卖精灵服务实现 + * 负责打开和操作跟卖精灵网站 + */ @Service public class GenmaiServiceImpl { @Value("${api.server.base-url}") @@ -21,6 +25,9 @@ public class GenmaiServiceImpl { private final RestTemplate restTemplate = new RestTemplate(); private final ObjectMapper objectMapper = new ObjectMapper(); + /** + * 打开跟卖精灵网站 + */ public void openGenmaiWebsite(Long accountId, String username) throws Exception { WebDriverManager.chromedriver() .driverRepositoryUrl(new URL("https://registry.npmmirror.com/-/binary/chromedriver/")) @@ -46,6 +53,9 @@ public class GenmaiServiceImpl { driver.navigate().refresh(); } + /** + * 获取并验证Token + */ @SuppressWarnings("unchecked") private String getAndValidateToken(Long accountId, String username) { String url = serverApiUrl + "/tool/genmai/accounts?name=" + username; @@ -68,6 +78,9 @@ public class GenmaiServiceImpl { return token; } + /** + * 验证Token是否有效 + */ private boolean validateToken(String token) { try { HttpHeaders headers = new HttpHeaders(); diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenCacheServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenCacheServiceImpl.java index 5db8844..a58af45 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenCacheServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenCacheServiceImpl.java @@ -1,4 +1,6 @@ package com.tashow.erp.service.impl; + +import com.tashow.erp.common.CacheConstants; import com.tashow.erp.entity.RakutenProductEntity; import com.tashow.erp.model.RakutenProduct; import com.tashow.erp.repository.RakutenProductRepository; @@ -61,10 +63,10 @@ public class RakutenCacheServiceImpl implements RakutenCacheService { .filter(name -> name != null && !name.trim().isEmpty()) .collect(Collectors.toSet()); - // 清理所有1小时前的旧数据,不分店铺全部清掉 - LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1); + // 清理所有过期的旧数据,不分店铺全部清掉 + LocalDateTime cutoffTime = LocalDateTime.now().minusHours(CacheConstants.RAKUTEN_CACHE_HOURS); repository.deleteAllDataBefore(cutoffTime); - log.info("清理1小时前的所有旧数据"); + log.info("清理{}小时前的所有旧数据", CacheConstants.RAKUTEN_CACHE_HOURS); List entities = products.stream() .map(product -> { @@ -80,7 +82,7 @@ public class RakutenCacheServiceImpl implements RakutenCacheService { } /** - * 检查店铺是否有1小时内的缓存数据(按用户隔离) + * 检查店铺是否有缓存时间内的缓存数据(按用户隔离) */ @Override public boolean hasRecentData(String shopName, String username) { @@ -88,9 +90,9 @@ public class RakutenCacheServiceImpl implements RakutenCacheService { return false; } boolean hasRecent = repository.existsByOriginalShopNameAndSessionIdStartingWithAndCreatedAtAfter( - shopName, username + "#", LocalDateTime.now().minusHours(1)); + shopName, username + "#", LocalDateTime.now().minusHours(CacheConstants.RAKUTEN_CACHE_HOURS)); if (hasRecent) { - log.info("店铺 {} 存在1小时内缓存数据(用户: {}),将使用缓存", shopName, username); + log.info("店铺 {} 存在缓存时间内缓存数据(用户: {}),将使用缓存", shopName, username); } return hasRecent; } @@ -114,6 +116,9 @@ public class RakutenCacheServiceImpl implements RakutenCacheService { /** * 更新指定店铺的所有产品的会话ID + * + * @param shopName 店铺名 + * @param newSessionId 新的会话ID */ @Override @Transactional @@ -131,6 +136,9 @@ public class RakutenCacheServiceImpl implements RakutenCacheService { /** * 更新指定产品列表的会话ID,只更新这些具体的产品 + * + * @param products 产品列表 + * @param newSessionId 新的会话ID */ @Override @Transactional @@ -167,7 +175,9 @@ public class RakutenCacheServiceImpl implements RakutenCacheService { } /** - * 清理指定店铺1小时之前的旧数据,保留1小时内的缓存 + * 清理指定店铺的旧数据,保留1小时内的缓存 + * + * @param shopName 店铺名 */ @Override @Transactional @@ -178,5 +188,4 @@ public class RakutenCacheServiceImpl implements RakutenCacheService { } - } \ No newline at end of file diff --git a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenScrapingServiceImpl.java b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenScrapingServiceImpl.java index cb066fa..2414f91 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenScrapingServiceImpl.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/service/impl/RakutenScrapingServiceImpl.java @@ -52,6 +52,9 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService { return products; } + /** + * 乐天页面解析器 + */ private class RakutenPageProcessor implements PageProcessor { private final List products; private final ErrorReporter errorReporter; @@ -60,6 +63,9 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService { this.errorReporter = errorReporter; } + /** + * 解析乐天页面数据 + */ @Override public void process(Page page) { List rankings = page.getHtml().xpath("//div[@class='srhRnk']/span[@class='icon']/text()").all(); diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/DataReportUtil.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/DataReportUtil.java index 1329d3c..df7848a 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/utils/DataReportUtil.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/DataReportUtil.java @@ -35,10 +35,9 @@ public class DataReportUtil { reportData.put("clientId", generateClientId(dataType)); reportData.put("dataType", dataType); reportData.put("dataCount", dataCount); - reportData.put("status", status); + reportData.put("status", status != null ? status : "0"); sendReportData(reportData); - logger.debug("数据上报成功: {} - {} 条", dataType, dataCount); } catch (Exception e) { logger.warn("数据上报失败: {}", e.getMessage()); } diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/ErrorReporter.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/ErrorReporter.java index d67b73d..01e8154 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/utils/ErrorReporter.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/ErrorReporter.java @@ -5,21 +5,17 @@ import java.util.concurrent.CompletableFuture; import java.io.StringWriter; import java.io.PrintWriter; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; import com.tashow.erp.service.impl.AuthServiceImpl; @Component public class ErrorReporter { - @Value("${api.server.base-url}") - private String serverUrl; - @Autowired 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("errorMessage", errorMessage); errorData.put("stackTrace", getStackTrace(ex)); - - // 添加系统信息 errorData.put("osName", System.getProperty("os.name")); errorData.put("osVersion", System.getProperty("os.version")); errorData.put("appVersion", System.getProperty("project.version", "unknown")); - String url = serverUrl + "/monitor/client/api/error"; - restTemplate.postForObject(url, errorData, Map.class); + apiForwarder.post("/monitor/error", errorData, null); } catch (Exception e) { - System.err.println("错误上报失败: " + e.getMessage()); + // 静默失败,不影响主业务 } }); } diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/StringUtils.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/StringUtils.java index f20c36d..0831f21 100644 --- a/erp_client_sb/src/main/java/com/tashow/erp/utils/StringUtils.java +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/StringUtils.java @@ -1,7 +1,6 @@ package com.tashow.erp.utils; - -import com.tashow.erp.common.Constants; +import com.tashow.erp.common.HttpConstants; import org.springframework.util.AntPathMatcher; import java.util.*; @@ -362,7 +361,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils */ 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; } + AntPathMatcher matcher = new AntPathMatcher(); for (String pattern : strs) { - if (isMatch(pattern, str)) + if (matcher.match(pattern, str)) { return true; } @@ -645,28 +645,6 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils 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 cast(Object obj) - { - return (T) obj; - } - /** * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 * diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/UrlBuilder.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/UrlBuilder.java new file mode 100644 index 0000000..35d8c02 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/UrlBuilder.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/java/com/tashow/erp/utils/ValidationUtils.java b/erp_client_sb/src/main/java/com/tashow/erp/utils/ValidationUtils.java new file mode 100644 index 0000000..4409793 --- /dev/null +++ b/erp_client_sb/src/main/java/com/tashow/erp/utils/ValidationUtils.java @@ -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() {} +} diff --git a/erp_client_sb/src/main/resources/application.yml b/erp_client_sb/src/main/resources/application.yml index 9360247..00a12cc 100644 --- a/erp_client_sb/src/main/resources/application.yml +++ b/erp_client_sb/src/main/resources/application.yml @@ -6,7 +6,6 @@ javafx: height: 800 # style: DECORATED # javafx.stage.StageStyle [DECORATED, UNDECORATED, TRANSPARENT, UTILITY, UNIFIED] # resizable: false - spring: main: lazy-initialization: true @@ -47,8 +46,9 @@ server: api: server: # 主服务器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://127.0.0.1:8085" paths: monitor: "/monitor/client/api" login: "/monitor/account/login" diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java index 6bd1822..bec2117 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java @@ -40,9 +40,6 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; import java.io.InputStream; -import java.util.Date; - - /** * 客户端账号控制器 * diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/VersionController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/VersionController.java index fe15827..9175424 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/VersionController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/VersionController.java @@ -10,6 +10,13 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.enums.BusinessType; 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.Map; @@ -25,26 +32,43 @@ public class VersionController extends BaseController { @Autowired private RedisTemplate redisTemplate; + @Autowired + private IClientAccountService clientAccountService; + @Autowired + private JwtRsaKeyService jwtRsaKeyService; + 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 JAR_URL_REDIS_KEY = "erp:client:jar_url"; 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") - public AjaxResult checkVersion(@RequestParam String currentVersion) { - String latestVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY); + public AjaxResult checkVersion(@RequestParam String currentVersion, HttpServletRequest request) { + 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; Map data = new HashMap<>(); data.put("currentVersion", currentVersion); data.put("latestVersion", latestVersion); data.put("needUpdate", needUpdate); - data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY)); - data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY)); - data.put("updateNotes", redisTemplate.opsForValue().get(UPDATE_NOTES_REDIS_KEY)); + data.put("asarUrl", redisTemplate.opsForValue().get(asarKey)); + data.put("jarUrl", redisTemplate.opsForValue().get(jarKey)); + data.put("updateNotes", redisTemplate.opsForValue().get(notesKey)); return AjaxResult.success(data); } @@ -54,16 +78,22 @@ public class VersionController extends BaseController { @PreAuthorize("@ss.hasPermi('system:version:query')") @GetMapping("/info") public AjaxResult getVersionInfo() { - String currentVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY); - if (StringUtils.isEmpty(currentVersion)) { - currentVersion = "2.0.0"; - } - Map data = new HashMap<>(); - data.put("currentVersion", currentVersion); - data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY)); - data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY)); - data.put("updateNotes", redisTemplate.opsForValue().get(UPDATE_NOTES_REDIS_KEY)); + + Map release = new HashMap<>(); + release.put("version", redisTemplate.opsForValue().get(VERSION_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 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()); return AjaxResult.success(data); @@ -78,25 +108,52 @@ public class VersionController extends BaseController { public AjaxResult updateVersionInfo(@RequestParam("version") String version, @RequestParam(value = "asarUrl", required = false) String asarUrl, @RequestParam(value = "jarUrl", required = false) String jarUrl, - @RequestParam("updateNotes") String updateNotes) { - redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version); + @RequestParam("updateNotes") String updateNotes, + @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)) { - redisTemplate.opsForValue().set(ASAR_URL_REDIS_KEY, asarUrl); + redisTemplate.opsForValue().set(asarKey, asarUrl); } 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 data = new HashMap<>(); data.put("version", version); data.put("asarUrl", asarUrl); data.put("jarUrl", jarUrl); data.put("updateNotes", updateNotes); + data.put("isBeta", isBeta); data.put("updateTime", System.currentTimeMillis()); 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 @@ -107,8 +164,12 @@ public class VersionController extends BaseController { if (StringUtils.isEmpty(version1) || StringUtils.isEmpty(version2)) { 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); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/service/impl/ClientMonitorServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/web/service/impl/ClientMonitorServiceImpl.java index 9f68cc4..ef3d958 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/service/impl/ClientMonitorServiceImpl.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/service/impl/ClientMonitorServiceImpl.java @@ -176,7 +176,6 @@ public class ClientMonitorServiceImpl implements IClientMonitorService { */ @Override public Map authenticateClient(String authKey, Map clientInfo) { - Map result = new HashMap<>(); try { String accessToken = UUID.randomUUID().toString().replace("-", ""); String clientId = (String) clientInfo.get("clientId"); @@ -194,16 +193,18 @@ public class ClientMonitorServiceImpl implements IClientMonitorService { clientMonitorMapper.updateClientOnlineStatus(clientId, "1"); } + Map result = new HashMap<>(); result.put("success", true); result.put("accessToken", accessToken); result.put("tokenType", "Bearer"); result.put("expiresIn", 7200); result.put("clientId", clientId); result.put("permissions", null); + return result; } catch (Exception e) { - throw new RuntimeException("认证失败: " + e.getMessage()); + logger.error("认证失败: {}", e.getMessage(), e); + throw new RuntimeException("认证失败", e); } - return result; } private ClientInfo findClientByClientId(String clientId) { @@ -225,17 +226,22 @@ public class ClientMonitorServiceImpl implements IClientMonitorService { */ @Override public void recordErrorReport(Map errorData) { - ClientErrorReport errorReport = new ClientErrorReport(); - errorReport.setClientId((String) errorData.get("clientId")); - errorReport.setErrorType((String) errorData.get("errorType")); - errorReport.setErrorMessage((String) errorData.get("errorMessage")); - errorReport.setStackTrace((String) errorData.get("stackTrace")); - errorReport.setErrorTime(DateUtils.getNowDate()); - errorReport.setUsername((String) errorData.get("username")); - errorReport.setOsName((String) errorData.get("osName")); - errorReport.setOsVersion((String) errorData.get("osVersion")); - errorReport.setAppVersion((String) errorData.get("appVersion")); - clientMonitorMapper.insertClientError(errorReport); + try { + ClientErrorReport errorReport = new ClientErrorReport(); + errorReport.setClientId((String) errorData.get("clientId")); + errorReport.setErrorType((String) errorData.get("errorType")); + errorReport.setErrorMessage((String) errorData.get("errorMessage")); + errorReport.setStackTrace((String) errorData.get("stackTrace")); + errorReport.setErrorTime(DateUtils.getNowDate()); + errorReport.setUsername((String) errorData.get("username")); + errorReport.setOsName((String) errorData.get("osName")); + errorReport.setOsVersion((String) errorData.get("osVersion")); + 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 { String clientId = (String) dataReport.get("clientId"); 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); ClientDataReport existingReport = clientMonitorMapper.findRecentDataReport(clientId, dataType, status); @@ -262,11 +269,15 @@ public class ClientMonitorServiceImpl implements IClientMonitorService { report.setStatus(status); clientMonitorMapper.insertDataReport(report); } - + if (clientId != null && !clientId.isEmpty()) { - clientMonitorMapper.updateClientOnlineStatus(clientId, "1"); + try { + clientMonitorMapper.updateClientOnlineStatus(clientId, "1"); + } catch (Exception ignored) {} } } catch (Exception e) { + logger.error("记录数据采集报告失败: {}", e.getMessage(), e); + throw new RuntimeException("记录数据采集报告失败", e); } } diff --git a/ruoyi-ui/src/api/monitor/version.js b/ruoyi-ui/src/api/monitor/version.js index 8d5cd31..7d8f942 100644 --- a/ruoyi-ui/src/api/monitor/version.js +++ b/ruoyi-ui/src/api/monitor/version.js @@ -32,7 +32,7 @@ export function uploadFile(data) { } // 更新版本信息和下载链接 -// data: { version, asarUrl, jarUrl } +// data: { version, asarUrl, jarUrl, updateNotes, isBeta } export function updateVersion(data) { return request({ url: '/system/version/update', diff --git a/ruoyi-ui/src/views/monitor/account/index.vue b/ruoyi-ui/src/views/monitor/account/index.vue index 61f4381..4f0a1f2 100644 --- a/ruoyi-ui/src/views/monitor/account/index.vue +++ b/ruoyi-ui/src/views/monitor/account/index.vue @@ -205,6 +205,7 @@ 工具箱功能 数据采集功能 1688比价功能 + 测试版更新权限 diff --git a/ruoyi-ui/src/views/monitor/version/index.vue b/ruoyi-ui/src/views/monitor/version/index.vue index ec2352d..e53c8d2 100644 --- a/ruoyi-ui/src/views/monitor/version/index.vue +++ b/ruoyi-ui/src/views/monitor/version/index.vue @@ -7,29 +7,42 @@ - - + +
- 当前版本信息 + 正式版
- - {{ versionInfo.currentVersion }} + + {{ versionInfo.release.version || '未设置' }} - - {{ parseTime(versionInfo.updateTime) }} + + {{ versionInfo.release.asarUrl }} - - - {{ versionInfo.asarUrl }} - + + {{ versionInfo.release.jarUrl }} - - - {{ versionInfo.jarUrl }} - + +
+
+
+ + +
+ 测试版 +
+
+ + + {{ versionInfo.beta.version || '未设置' }} + + + {{ versionInfo.beta.asarUrl }} + + + {{ versionInfo.beta.jarUrl }}
@@ -40,8 +53,14 @@ + + + 正式版 + 测试版 + + - + @@ -98,10 +117,9 @@ export default { showSearch: true, // 版本信息 versionInfo: { - currentVersion: '2.0.0', - updateTime: null, - asarUrl: null, - jarUrl: null + release: { version: null, asarUrl: null, jarUrl: null, updateNotes: null }, + beta: { version: null, asarUrl: null, jarUrl: null, updateNotes: null }, + updateTime: null }, // 版本检查表单 checkForm: { @@ -115,6 +133,7 @@ export default { uploadVisible: false, // 上传表单 uploadForm: { + isBeta: false, version: '', updateNotes: '', asarFile: null, @@ -124,7 +143,16 @@ export default { uploadRules: { version: [ { 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: [ { required: true, message: "更新内容不能为空", trigger: "blur" } @@ -158,6 +186,7 @@ export default { /** 重置上传表单 */ resetUploadForm() { this.uploadForm = { + isBeta: false, version: '', updateNotes: '', asarFile: null, @@ -260,7 +289,8 @@ export default { version: this.uploadForm.version, asarUrl: asarUrl, jarUrl: jarUrl, - updateNotes: this.uploadForm.updateNotes + updateNotes: this.uploadForm.updateNotes, + isBeta: this.uploadForm.isBeta }).then(() => { this.$modal.msgSuccess("版本文件上传成功"); this.uploadVisible = false;