This commit is contained in:
2025-09-30 17:16:11 +08:00
parent e650a7c7f3
commit 52ce0e1969
25 changed files with 689 additions and 989 deletions

465
.idea/workspace.xml generated
View File

@@ -1,465 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="71d19dd6-7472-4ebf-b309-b7afee3f99de" name="更改" comment="1">
<change afterPath="$PROJECT_DIR$/electron-vue-template/public/erp_client_sb-2.4.7.jar" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.claude/settings.local.json" beforeDir="false" afterPath="$PROJECT_DIR$/.claude/settings.local.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/data/device.id" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/data/erp-cache.db" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/data/erp-cache.db-shm" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/data/erp-cache.db-wal" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/data/jwt_rsa_private.pem" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/data/jwt_rsa_public.pem" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/electron-builder.json" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/electron-builder.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/main/main.ts" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/main/main.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/main/preload.ts" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/main/preload.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/App.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/App.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/api/http.ts" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/api/http.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/api/zebra.ts" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/api/zebra.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/common/AccountManager.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/common/AccountManager.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/common/UpdateDialog.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/common/UpdateDialog.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/layout/NavigationBar.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/layout/NavigationBar.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/erp_client_sb/data/device.id" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/erp_client_sb/data/erp-cache.db" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java" beforeDir="false" afterPath="$PROJECT_DIR$/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="userSettingsFile" value="C:\Program Files\apache-maven-3.9.4\conf\settings.xml" />
</MavenGeneralSettings>
</option>
</component>
<component name="MavenRunner">
<option name="skipTests" value="true" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="332JslhtSnNRRZRMrLiHaPZ3q2S" />
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Maven.erp_client_sb [clean].executor&quot;: &quot;Run&quot;,
&quot;Maven.erp_client_sb [install].executor&quot;: &quot;Run&quot;,
&quot;Maven.erp_client_sb [verify].executor&quot;: &quot;Run&quot;,
&quot;Maven.ruoyi [clean].executor&quot;: &quot;Run&quot;,
&quot;Maven.ruoyi [install].executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RequestMappingsPanelOrder0&quot;: &quot;0&quot;,
&quot;RequestMappingsPanelOrder1&quot;: &quot;1&quot;,
&quot;RequestMappingsPanelWidth0&quot;: &quot;75&quot;,
&quot;RequestMappingsPanelWidth1&quot;: &quot;75&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;Spring Boot.ErpClientSbApplication.executor&quot;: &quot;Debug&quot;,
&quot;Spring Boot.RuoYiApplication.executor&quot;: &quot;Debug&quot;,
&quot;Spring Boot.未命名.executor&quot;: &quot;Debug&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/ZiJIe/Desktop/wox/RuoYi-Vue/electron-vue-template/public&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;npm.build.executor&quot;: &quot;Run&quot;,
&quot;npm.build:win.executor&quot;: &quot;Run&quot;,
&quot;npm.run.executor&quot;: &quot;Run&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;com.intellij.platform.ide.impl.presentationAssistant.PresentationAssistantConfigurable&quot;,
&quot;ts.external.directory.path&quot;: &quot;C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\electron-vue-template\\node_modules\\typescript\\lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="ReactorSettings">
<option name="notificationShown" value="true" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\electron-vue-template\public" />
<recent name="C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\electron-vue-template\dist\win-unpacked" />
<recent name="C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\electron-vue-template\dist\win-unpacked" />
</key>
</component>
<component name="RunDashboard">
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType" />
<option value="js.build_tools.npm" />
</set>
</option>
</component>
<component name="RunManager" selected="npm.build:win">
<configuration default="true" type="JetRunConfigurationType">
<module name="RuoYi-Vue" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
<module name="RuoYi-Vue" />
<option name="filePath" />
<method v="2" />
</configuration>
<configuration name="ErpClientSbApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ALTERNATIVE_JRE_PATH" value="17" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<module name="erp_client_sb" />
<option name="SPRING_BOOT_MAIN_CLASS" value="com.tashow.erp.ErpClientSbApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="RuoYiApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ALTERNATIVE_JRE_PATH" value="17" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<module name="ruoyi-admin" />
<option name="SPRING_BOOT_MAIN_CLASS" value="com.ruoyi.RuoYiApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="build" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/electron-vue-template/package.json" />
<command value="run" />
<scripts>
<script value="build" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
<configuration name="build:win" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/electron-vue-template/package.json" />
<command value="run" />
<scripts>
<script value="build:win" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
<configuration name="run" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/electron-vue-template/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
<list>
<item itemvalue="npm.run" />
<item itemvalue="npm.build" />
<item itemvalue="npm.build:win" />
<item itemvalue="Spring Boot.RuoYiApplication" />
<item itemvalue="Spring Boot.ErpClientSbApplication" />
</list>
<recent_temporary>
<list>
<item itemvalue="npm.build:win" />
<item itemvalue="npm.build" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.23774.435" />
<option value="bundled-js-predefined-d6986cc7102b-f27c65a3e318-JavaScript-IU-251.23774.435" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="71d19dd6-7472-4ebf-b309-b7afee3f99de" name="更改" comment="" />
<created>1758509443816</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1758509443816</updated>
<workItem from="1758509446082" duration="2966000" />
<workItem from="1758512571699" duration="311000" />
<workItem from="1758694285287" duration="16954000" />
<workItem from="1758765346934" duration="13417000" />
<workItem from="1758780949720" duration="2793000" />
<workItem from="1758783772919" duration="3262000" />
<workItem from="1758787264618" duration="6950000" />
<workItem from="1758794319987" duration="13284000" />
<workItem from="1758860156056" duration="54529000" />
<workItem from="1759112919536" duration="1025000" />
<workItem from="1759114000963" duration="12514000" />
<workItem from="1759130476790" duration="1021000" />
<workItem from="1759131666876" duration="1987000" />
<workItem from="1759133675854" duration="9993000" />
</task>
<task id="LOCAL-00001" summary="1">
<option name="closed" value="true" />
<created>1758511833782</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1758511833782</updated>
</task>
<task id="LOCAL-00002" summary="1">
<option name="closed" value="true" />
<created>1758512348322</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1758512348322</updated>
</task>
<task id="LOCAL-00003" summary="1">
<option name="closed" value="true" />
<created>1758521046022</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1758521046022</updated>
</task>
<task id="LOCAL-00004" summary="1">
<option name="closed" value="true" />
<created>1758522202417</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1758522202417</updated>
</task>
<task id="LOCAL-00005" summary="1">
<option name="closed" value="true" />
<created>1758522758523</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1758522758523</updated>
</task>
<task id="LOCAL-00006" summary="1">
<option name="closed" value="true" />
<created>1758523822682</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1758523822682</updated>
</task>
<task id="LOCAL-00007" summary="1">
<option name="closed" value="true" />
<created>1758524938236</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1758524938236</updated>
</task>
<task id="LOCAL-00008" summary="1">
<option name="closed" value="true" />
<created>1758525299299</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1758525299299</updated>
</task>
<task id="LOCAL-00009" summary="1">
<option name="closed" value="true" />
<created>1758525500986</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1758525500986</updated>
</task>
<task id="LOCAL-00010" summary="1">
<option name="closed" value="true" />
<created>1758526085800</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1758526085800</updated>
</task>
<task id="LOCAL-00011" summary="1">
<option name="closed" value="true" />
<created>1758528696003</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1758528696003</updated>
</task>
<task id="LOCAL-00012" summary="1">
<option name="closed" value="true" />
<created>1758529627894</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1758529627894</updated>
</task>
<task id="LOCAL-00013" summary="1">
<option name="closed" value="true" />
<created>1758619260259</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1758619260259</updated>
</task>
<task id="LOCAL-00014" summary="1">
<option name="closed" value="true" />
<created>1758619552979</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1758619552979</updated>
</task>
<task id="LOCAL-00015" summary="1">
<option name="closed" value="true" />
<created>1758683139068</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1758683139068</updated>
</task>
<task id="LOCAL-00016" summary="1">
<option name="closed" value="true" />
<created>1758683285305</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1758683285305</updated>
</task>
<task id="LOCAL-00017" summary="1">
<option name="closed" value="true" />
<created>1758683484212</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1758683484212</updated>
</task>
<task id="LOCAL-00018" summary="1">
<option name="closed" value="true" />
<created>1758787441900</created>
<option name="number" value="00018" />
<option name="presentableId" value="LOCAL-00018" />
<option name="project" value="LOCAL" />
<updated>1758787441900</updated>
</task>
<task id="LOCAL-00019" summary="1">
<option name="closed" value="true" />
<created>1758787531138</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1758787531138</updated>
</task>
<task id="LOCAL-00020" summary="1">
<option name="closed" value="true" />
<created>1758787581342</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1758787581342</updated>
</task>
<task id="LOCAL-00021" summary="1">
<option name="closed" value="true" />
<created>1758787954763</created>
<option name="number" value="00021" />
<option name="presentableId" value="LOCAL-00021" />
<option name="project" value="LOCAL" />
<updated>1758787954763</updated>
</task>
<task id="LOCAL-00022" summary="1">
<option name="closed" value="true" />
<created>1758788061529</created>
<option name="number" value="00022" />
<option name="presentableId" value="LOCAL-00022" />
<option name="project" value="LOCAL" />
<updated>1758788061529</updated>
</task>
<task id="LOCAL-00023" summary="1">
<option name="closed" value="true" />
<created>1758795230077</created>
<option name="number" value="00023" />
<option name="presentableId" value="LOCAL-00023" />
<option name="project" value="LOCAL" />
<updated>1758795230077</updated>
</task>
<task id="LOCAL-00024" summary="1">
<option name="closed" value="true" />
<created>1758875248722</created>
<option name="number" value="00024" />
<option name="presentableId" value="LOCAL-00024" />
<option name="project" value="LOCAL" />
<updated>1758875248722</updated>
</task>
<task id="LOCAL-00025" summary="1">
<option name="closed" value="true" />
<created>1758877545022</created>
<option name="number" value="00025" />
<option name="presentableId" value="LOCAL-00025" />
<option name="project" value="LOCAL" />
<updated>1758877545022</updated>
</task>
<task id="LOCAL-00026" summary="1">
<option name="closed" value="true" />
<created>1758953146637</created>
<option name="number" value="00026" />
<option name="presentableId" value="LOCAL-00026" />
<option name="project" value="LOCAL" />
<updated>1758953146637</updated>
</task>
<task id="LOCAL-00027" summary="1">
<option name="closed" value="true" />
<created>1758966134103</created>
<option name="number" value="00027" />
<option name="presentableId" value="LOCAL-00027" />
<option name="project" value="LOCAL" />
<updated>1758966134103</updated>
</task>
<task id="LOCAL-00028" summary="1">
<option name="closed" value="true" />
<created>1759129266815</created>
<option name="number" value="00028" />
<option name="presentableId" value="LOCAL-00028" />
<option name="project" value="LOCAL" />
<updated>1759129266815</updated>
</task>
<option name="localTasksCounter" value="29" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="1" />
<option name="LAST_COMMIT_MESSAGE" value="1" />
</component>
</project>

View File

@@ -57,26 +57,43 @@ function getJavaExecutablePath(): string {
return 'java';
}
function findJarFile(directory: string): string {
if (!existsSync(directory)) return '';
const files = require('fs').readdirSync(directory);
const jarFile = files.find((f: string) => f.startsWith('erp_client_sb-') && f.endsWith('.jar'));
return jarFile ? join(directory, jarFile) : '';
}
function extractVersionFromJar(jarPath: string): string {
if (!jarPath) return '';
const match = require('path').basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/);
return match?.[1] || '';
}
function getJarFilePath(): string {
if (process.env.NODE_ENV === 'development') {
return join(__dirname, '../../public/erp_client_sb-2.4.7.jar');
return findJarFile(join(__dirname, '../../public'));
}
// 生产环境需要将JAR包从asar提取到临时位置
const tempDir = join(app.getPath('temp'), 'erp-client');
const tempJarPath = join(tempDir, 'erp_client_sb-2.4.7.jar');
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
// 确保临时目录存在
if (!existsSync(tempDir)) {
mkdirSync(tempDir, { recursive: true });
const asarJarPath = findJarFile(join(__dirname, '../assets'));
if (!asarJarPath) return '';
const asarFileName = require('path').basename(asarJarPath);
const tempJarPath = join(tempDir, asarFileName);
// 如果临时目录版本不同,删除旧版本并复制新版本
const existingJar = findJarFile(tempDir);
if (existingJar && require('path').basename(existingJar) !== asarFileName) {
require('fs').unlinkSync(existingJar);
}
// 如果临时JAR不存在从asar中复制
if (!existsSync(tempJarPath)) {
const asarJarPath = join(__dirname, '../assets/erp_client_sb-2.4.7.jar');
if (existsSync(asarJarPath)) {
copyFileSync(asarJarPath, tempJarPath);
}
copyFileSync(asarJarPath, tempJarPath);
}
return tempJarPath;
@@ -213,7 +230,7 @@ function startSpringBoot() {
}
}
//startSpringBoot();
startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
@@ -299,9 +316,9 @@ app.whenReady().then(() => {
}
//11111
setTimeout(() => {
openAppIfNotOpened();
}, 2000);
// setTimeout(() => {
// openAppIfNotOpened();
// }, 2000);
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
@@ -323,6 +340,8 @@ ipcMain.on('message', (event, message) => {
console.log(message);
});
ipcMain.handle('get-jar-version', () => extractVersionFromJar(getJarFilePath()));
function checkPendingUpdate() {
try {
const updateFilePath = join(process.resourcesPath, 'app.asar.update');

View File

@@ -3,6 +3,8 @@ import { contextBridge, ipcRenderer } from 'electron'
const electronAPI = {
sendMessage: (message: string) => ipcRenderer.send('message', message),
getJarVersion: () => ipcRenderer.invoke('get-jar-version'),
downloadUpdate: (downloadUrl: string) => ipcRenderer.invoke('download-update', downloadUrl),
getDownloadProgress: () => ipcRenderer.invoke('get-download-progress'),
installUpdate: () => ipcRenderer.invoke('install-update'),

View File

@@ -134,56 +134,43 @@ function handleMenuSelect(key: string) {
async function handleLoginSuccess(data: { token: string; permissions?: string }) {
isAuthenticated.value = true
showAuthDialog.value = false
showRegDialog.value = false // 确保注册对话框也关闭
showRegDialog.value = false
try {
// 保存token到本地数据库
await authApi.saveToken(data.token)
const username = getUsernameFromToken(data.token)
currentUsername.value = username
userPermissions.value = data?.permissions || ''
await deviceApi.register({username})
// 建立SSE连接
SSEManager.connect()
} catch (e: any) {
// 设备注册失败时回滚登录状态
isAuthenticated.value = false
showAuthDialog.value = true
await authApi.deleteTokenCache()
ElMessage({
message: e?.message || '设备注册失败,请重试',
type: 'error'
})
ElMessage.error(e?.message || '设备注册失败')
}
}
async function logout() {
// 主动设置设备离线
try {
const deviceId = await getClientIdFromToken()
if (deviceId) {
await deviceApi.offline({ deviceId })
}
if (deviceId) await deviceApi.offline({ deviceId })
} catch (error) {
console.warn('离线通知失败:', error)
}
const token = await authApi.getToken()
if (token) {
await authApi.logout(token)
}
try {
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (token) await authApi.logout(token)
} catch {}
await authApi.deleteTokenCache()
// 清理前端状态
isAuthenticated.value = false
currentUsername.value = ''
userPermissions.value = ''
showAuthDialog.value = true
showDeviceDialog.value = false
// 关闭SSE连接
SSEManager.disconnect()
}
@@ -199,12 +186,8 @@ async function handleUserClick() {
cancelButtonText: '取消'
})
await logout()
ElMessage({
message: '已退出登录',
type: 'success'
})
} catch {
}
ElMessage.success('已退出登录')
} catch {}
}
function showRegisterDialog() {
@@ -218,26 +201,23 @@ function backToLogin() {
}
async function checkAuth() {
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
try {
await authApi.sessionBootstrap().catch(() => undefined)
const token = await authApi.getToken()
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (token) {
const response = await authApi.verifyToken(token)
if (response?.success) {
isAuthenticated.value = true
currentUsername.value = getUsernameFromToken(token) || ''
SSEManager.connect()
return
}
await authApi.deleteTokenCache()
await authApi.verifyToken(token)
isAuthenticated.value = true
currentUsername.value = getUsernameFromToken(token) || ''
SSEManager.connect()
return
}
} catch {
// 忽略
await authApi.deleteTokenCache()
}
if (authRequiredMenus.includes(activeMenu.value)) {
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
showAuthDialog.value = true
}
}
@@ -246,11 +226,11 @@ async function getClientIdFromToken(token?: string) {
try {
let t = token
if (!t) {
t = await authApi.getToken()
const tokenRes: any = await authApi.getToken()
t = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
}
if (!t) return ''
const payload = JSON.parse(atob(t.split('.')[1] || ''))
const payload = JSON.parse(atob(t.split('.')[1]))
return payload.clientId || ''
} catch {
return ''
@@ -259,7 +239,7 @@ async function getClientIdFromToken(token?: string) {
function getUsernameFromToken(token: string) {
try {
const payload = JSON.parse(atob(token.split('.')[1] || ''))
const payload = JSON.parse(atob(token.split('.')[1]))
return payload.username || ''
} catch {
return ''
@@ -273,21 +253,24 @@ const SSEManager = {
if (this.connection) return
try {
const token = await authApi.getToken()
if (!token) return
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (!token) {
console.warn('SSE连接失败: 没有有效的 token')
return
}
const clientId = await getClientIdFromToken(token)
if (!clientId) return
if (!clientId) {
console.warn('SSE连接失败: 无法从 token 获取 clientId')
return
}
let sseUrl = 'http://192.168.1.89:8080/monitor/account/events'
try {
const resp = await fetch('/api/config/server')
if (resp.ok) {
const config = await resp.json()
sseUrl = config.sseUrl || sseUrl
}
} catch {
}
if (resp.ok) sseUrl = (await resp.json()).sseUrl || sseUrl
} catch {}
const src = new EventSource(`${sseUrl}?clientId=${clientId}&token=${token}`)
this.connection = src
@@ -296,15 +279,13 @@ const SSEManager = {
src.onerror = () => this.handleError()
} catch (e: any) {
console.warn('SSE连接失败:', e?.message || e)
this.disconnect()
}
},
handleMessage(e: MessageEvent) {
try {
// 处理ping心跳
if (e.type === 'ping') {
return // ping消息自动保持连接无需处理
}
if (e.type === 'ping') return
console.log('SSE消息:', e.data)
const payload = JSON.parse(e.data)
@@ -314,17 +295,11 @@ const SSEManager = {
break
case 'DEVICE_REMOVED':
logout()
ElMessage({
message: '您的设备已被移除,请重新登录',
type: 'warning'
})
ElMessage.warning('您的设备已被移除,请重新登录')
break
case 'FORCE_LOGOUT':
logout()
ElMessage({
message: '会话已失效,请重新登录',
type: 'warning'
})
ElMessage.warning('会话已失效,请重新登录')
break
case 'PERMISSIONS_UPDATED':
checkAuth()
@@ -336,18 +311,16 @@ const SSEManager = {
},
handleError() {
this.disconnect()
setTimeout(() => this.connect(), 3000)
if (!this.connection) return
try { this.connection.close() } catch {}
this.connection = null
console.warn('SSE连接失败,已断开')
},
disconnect() {
if (this.connection) {
try {
this.connection.close()
} catch {
}
this.connection = null
}
if (!this.connection) return
try { this.connection.close() } catch {}
this.connection = null
},
}
@@ -366,26 +339,22 @@ function openSettings() {
async function fetchDeviceData() {
if (!currentUsername.value) {
ElMessage({
message: '未获取到用户名,请重新登录',
type: 'warning'
})
ElMessage.warning('未获取到用户名,请重新登录')
return
}
try {
deviceLoading.value = true
const [quota, list] = await Promise.all([
const [quotaRes, listRes] = await Promise.all([
deviceApi.getQuota(currentUsername.value),
deviceApi.list(currentUsername.value),
])
deviceQuota.value = quota || {limit: 0, used: 0}
]) as any[]
deviceQuota.value = quotaRes?.data || quotaRes || {limit: 0, used: 0}
const clientId = await getClientIdFromToken()
devices.value = (list || []).map(d => ({...d, isCurrent: d.deviceId === clientId})) as any
const list = listRes?.data || listRes || []
devices.value = list.map(d => ({...d, isCurrent: d.deviceId === clientId}))
} catch (e: any) {
ElMessage({
message: e?.message || '获取设备列表失败',
type: 'error'
})
ElMessage.error(e?.message || '获取设备列表失败')
} finally {
deviceLoading.value = false
}
@@ -403,21 +372,12 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
// 如果是本机设备被移除执行logout
const clientId = await getClientIdFromToken()
if (row.deviceId === clientId) {
await logout()
}
if (row.deviceId === clientId) await logout()
ElMessage({
message: '已移除设备',
type: 'success'
})
ElMessage.success('已移除设备')
} catch (e: any) {
ElMessage({
message: '移除设备失败: ' + ((e as any)?.message || '未知错误'),
type: 'error'
})
if (e !== 'cancel') ElMessage.error('移除设备失败: ' + (e?.message || '未知错误'))
}
}

View File

@@ -1,113 +1,39 @@
import { http } from './http';
// 统一响应处理函数 - 适配ERP客户端格式
function unwrap<T>(res: any): T {
if (res && typeof res.success === 'boolean') {
if (!res.success) {
const message: string = res.message || res.msg || '请求失败';
throw new Error(message);
}
return res as T;
}
// 兼容标准格式
if (res && typeof res.code === 'number') {
if (res.code !== 0) {
const message: string = res.msg || '请求失败';
throw new Error(message);
}
return (res.data as T) ?? ({} as T);
}
return res as T;
}
// 认证相关类型定义
interface LoginRequest {
username: string;
password: string;
}
interface RegisterRequest {
username: string;
password: string;
}
interface LoginResponse {
success: boolean;
token: string;
permissions: string[];
username: string;
message?: string;
}
interface RegisterResponse {
success: boolean;
message?: string;
}
import { http } from './http'
export const authApi = {
// 用户登录
login(params: LoginRequest) {
return http
.post('/api/login', params)
.then(res => unwrap<LoginResponse>(res));
login(params: { username: string; password: string }) {
return http.post('/api/login', params)
},
// 用户注册
register(params: RegisterRequest) {
return http
.post('/api/register', params)
.then(res => unwrap<RegisterResponse>(res));
register(params: { username: string; password: string }) {
return http.post('/api/register', params)
},
// 检查用户名可用性
checkUsername(username: string) {
return http
.get('/api/check-username', { username })
.then(res => {
if (res && res.code === 200) {
return { available: res.data };
}
throw new Error(res?.msg || '检查用户名失败');
});
return http.get('/api/check-username', { username })
},
// 验证token有效性
verifyToken(token: string) {
return http
.post('/api/verify', { token })
.then(res => unwrap<{ success: boolean }>(res));
return http.post('/api/verify', { token })
},
// 用户登出
logout(token: string) {
return http.postVoid('/api/logout', { token });
return http.postVoid('/api/logout', { token })
},
// 删除token缓存
deleteTokenCache() {
return http.postVoid('/api/cache/delete?key=token');
return http.postVoid('/api/cache/delete?key=token')
},
// 保存token到本地数据库
saveToken(token: string) {
return http.postVoid('/api/cache/save', { key: 'token', value: token });
return http.postVoid('/api/cache/save', { key: 'token', value: token })
},
// 从本地数据库获取token
getToken(): Promise<string | undefined> {
return http.get<any>('/api/cache/get?key=token').then((res: any) => {
if (typeof res === 'string') return res;
if (res && typeof res === 'object') {
if (typeof res.code === 'number') {
return res.code === 0 ? (res.data as string | undefined) : undefined;
}
if (typeof (res as any).data === 'string') return (res as any).data as string;
}
return undefined;
});
getToken() {
return http.get('/api/cache/get?key=token')
},
// 会话引导:检查并恢复会话(返回体各异,这里保持 any
sessionBootstrap() {
return http.get<any>('/api/session/bootstrap');
},
};
return http.get('/api/session/bootstrap')
}
}

View File

@@ -1,52 +1,27 @@
import { http } from './http'
// 与老版保持相同的接口路径与参数
const base = '/api/device'
export interface DeviceQuota {
limit: number
used: number
}
export interface DeviceItem {
deviceId: string
name?: string
status?: 'online' | 'offline'
lastActiveAt?: string
}
// 统一处理AjaxResult格式
function handleAjaxResult(res: any) {
if (res?.code !== 200) {
throw new Error(res?.msg || '操作失败')
}
return res.data
}
export const deviceApi = {
getQuota(username: string): Promise<DeviceQuota> {
return http.get(`${base}/quota`, { username }).then(handleAjaxResult)
getQuota(username: string) {
return http.get('/api/device/quota', { username })
},
list(username: string): Promise<DeviceItem[]> {
return http.get(`${base}/list`, { username }).then(handleAjaxResult)
list(username: string) {
return http.get('/api/device/list', { username })
},
register(payload: { username: string }) {
return http.post(`${base}/register`, payload).then(handleAjaxResult)
return http.post('/api/device/register', payload)
},
remove(payload: { deviceId: string }) {
return http.post(`${base}/remove`, payload).then(handleAjaxResult)
return http.post('/api/device/remove', payload)
},
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
return http.post(`${base}/heartbeat`, payload).then(handleAjaxResult)
return http.post('/api/device/heartbeat', payload)
},
offline(payload: { deviceId: string }) {
return http.post(`${base}/offline`, payload).then(handleAjaxResult)
},
return http.post('/api/device/offline', payload)
}
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
import { http } from './http'
export const updateApi = {
getVersion() {
return http.get('/api/update/version')
},
checkUpdate(currentVersion: string) {
return http.get(`/system/version/check?currentVersion=${currentVersion}`)
}
}

View File

@@ -1,79 +1,39 @@
// 斑马订单模型(根据页面所需字段精简定义)
export interface ZebraOrder {
orderedAt?: string;
productImage?: string;
productTitle?: string;
shopOrderNumber?: string;
timeSinceOrder?: string;
priceJpy?: number;
productQuantity?: number;
shippingFeeJpy?: number;
serviceFee?: string;
productNumber?: string;
poNumber?: string;
shippingFeeCny?: number;
internationalShippingFee?: number;
poLogisticsCompany?: string;
poTrackingNumber?: string;
internationalTrackingNumber?: string;
trackInfo?: string;
}
import { http } from './http'
export interface ZebraOrdersResp {
orders: ZebraOrder[];
total?: number;
totalPages?: number;
}
import { http } from './http';
export interface BanmaAccount {
id?: number;
name?: string;
username?: string;
password?: string;
token?: string;
tokenExpireAt?: string | number;
isDefault?: number;
status?: number;
remark?: string;
}
// 斑马 API与原 zebra-api.js 对齐的接口封装
export const zebraApi = {
// 账号管理ruoyi-admin
getAccounts() {
return http.get<{ code?: number; msg?: string; data: BanmaAccount[] }>('/tool/banma/accounts');
return http.get('/tool/banma/accounts')
},
saveAccount(body: BanmaAccount) {
return http.post<{ id: number }>('/tool/banma/accounts', body);
saveAccount(body: any) {
return http.post('/tool/banma/accounts', body)
},
removeAccount(id: number) {
// 用 postVoid 也可,但这里前端未用到,保留以备将来
return http.delete<void>(`/tool/banma/accounts/${id}`);
return http.delete(`/tool/banma/accounts/${id}`)
},
// 业务采集
getShops(params?: { accountId?: number }) {
return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>(
'/api/banma/shops', params as unknown as Record<string, unknown>
);
},
getOrders(params: { accountId?: number; startDate?: string; endDate?: string; page?: number; pageSize?: number; shopIds?: string; batchId: string }) {
return http.get<ZebraOrdersResp>('/api/banma/orders', params as unknown as Record<string, unknown>);
return http.get('/api/banma/shops', params as Record<string, unknown>)
},
getOrders(params: any) {
return http.get('/api/banma/orders', params as Record<string, unknown>)
},
// 其他功能(客户端微服务)
getOrdersByBatch(batchId: string) {
return http.get<ZebraOrdersResp>(`/api/banma/orders/batch/${batchId}`);
return http.get(`/api/banma/orders/batch/${batchId}`)
},
getLatestOrders() {
return http.get<ZebraOrdersResp>('/api/banma/orders/latest');
return http.get('/api/banma/orders/latest')
},
getOrderStats() {
return http.get('/api/banma/orders/stats');
return http.get('/api/banma/orders/stats')
},
searchOrders(searchParams: Record<string, unknown>) {
return http.get('/api/banma/orders/search', searchParams);
},
};
return http.get('/api/banma/orders/search', searchParams)
}
}

View File

@@ -295,6 +295,17 @@ onMounted(async () => {
<div class="body-layout">
<!-- 左侧步骤栏 -->
<aside class="steps-sidebar">
<!-- 顶部标签栏 -->
<div class="top-tabs">
<div class="tab-item active">
<span class="tab-icon">📦</span>
<span class="tab-text">ASIN查询</span>
</div>
<div class="tab-item" @click="openGenmaiSpirit">
<span class="tab-icon">🔍</span>
<span class="tab-text">跟卖精灵</span>
</div>
</div>
<div class="steps-title">操作流程</div>
<div class="steps-flow">
<!-- 1 -->
@@ -356,7 +367,6 @@ onMounted(async () => {
</div>
<div class="export-progress-text">{{ Math.round(exportProgress) }}%</div>
</div>
<el-button size="small" class="w100 btn-blue" :loading="genmaiLoading" @click="openGenmaiSpirit">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</el-button>
</div>
</div>
</div>
@@ -453,14 +463,25 @@ onMounted(async () => {
<style scoped>
.amazon-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
.body-layout { display: flex; gap: 12px; height: 100%; }
/* 顶部标签栏 */
.top-tabs { display: flex; margin-bottom: 8px; }
.tab-item { flex: 1; display: flex; align-items: center; justify-content: center; gap: 3px; padding: 4px 6px; cursor: pointer; transition: all 0.2s ease; background: #f5f7fa; color: #606266; font-size: 11px; font-weight: 500; border: 1px solid #ebeef5; }
.tab-item:first-child { border-radius: 3px 0 0 3px; }
.tab-item:last-child { border-radius: 0 3px 3px 0; border-left: none; }
.tab-item:hover { background: #e8f4ff; color: #409EFF; }
.tab-item.active { background: #1677FF; color: #fff; border-color: #1677FF; cursor: default; }
.tab-icon { font-size: 12px; }
.tab-text { line-height: 1; }
.body-layout { display: flex; gap: 12px; flex: 1; overflow: hidden; }
.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; margin-bottom: 8px; text-align: left; }
.steps-title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
.steps-flow { position: relative; }
.steps-flow:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
.flow-item { position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 10px; padding: 8px 0; }
.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 + .flow-item { border-top: 1px dashed #ebeef5; }
.flow-item .step-index { position: static; width: 24px; height: 24px; line-height: 24px; 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; }
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
@@ -521,14 +542,14 @@ onMounted(async () => {
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: center; }
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; text-align: center; }
.table tbody tr:hover { background: #f9f9f9; }
.table th:nth-child(1), .table td:nth-child(1) { width: 33.33%; }
.table th:nth-child(2), .table td:nth-child(2) { width: 33.33%; }
.table th:nth-child(3), .table td:nth-child(3) { width: 33.33%; }
.asin-out { color: #f56c6c; font-weight: 600; }
.seller-info { display: flex; align-items: center; gap: 4px; }
.seller-info { display: flex; align-items: center; gap: 4px; justify-content: center; }
.seller { color: #303133; font-weight: 500; }
.shipper { color: #909399; font-size: 12px; }
.price { color: #e6a23c; font-weight: 600; }

View File

@@ -31,11 +31,10 @@ async function handleAuth() {
authLoading.value = true
try {
// 1. 先检查设备限制
await deviceApi.register({ username: authForm.value.username })
const loginRes: any = await authApi.login(authForm.value)
const data = loginRes?.data || loginRes
// 2. 设备检查通过,进行登录
const data = await authApi.login(authForm.value)
emit('loginSuccess', {
token: data.token,
user: {

View File

@@ -41,8 +41,9 @@ async function checkUsernameAvailability() {
}
try {
const data = await authApi.checkUsername(registerForm.value.username)
usernameCheckResult.value = data.available
const res: any = await authApi.checkUsername(registerForm.value.username)
const data = res?.data || res
usernameCheckResult.value = data?.available || false
} catch {
usernameCheckResult.value = null
}
@@ -53,17 +54,16 @@ async function handleRegister() {
registerLoading.value = true
try {
// 1. 注册
await authApi.register({
username: registerForm.value.username,
password: registerForm.value.password
})
// 2. 注册成功后直接登录
const loginData = await authApi.login({
const loginRes: any = await authApi.login({
username: registerForm.value.username,
password: registerForm.value.password
})
const loginData = loginRes?.data || loginRes
emit('loginSuccess', {
token: loginData.token,

View File

@@ -0,0 +1,332 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
getSettings,
saveSettings,
savePlatformSettings,
type Platform,
type PlatformExportSettings
} from '../../utils/settings'
interface Props {
modelValue: boolean
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// 平台配置
const platforms = [
{ key: 'amazon' as Platform, name: 'Amazon', icon: '🛒', color: '#FF9900' },
{ key: 'rakuten' as Platform, name: 'Rakuten', icon: '🛍️', color: '#BF0000' },
{ key: 'zebra' as Platform, name: 'Zebra', icon: '🦓', color: '#34495E' }
]
// 设置项
const platformSettings = ref<Record<Platform, PlatformExportSettings>>({
amazon: { exportPath: '' },
rakuten: { exportPath: '' },
zebra: { exportPath: '' }
})
const activeTab = ref<Platform>('amazon')
const show = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 选择导出路径
async function selectExportPath(platform: Platform) {
const result = await (window as any).electronAPI.showOpenDialog({
title: `选择${platforms.find(p => p.key === platform)?.name}默认导出路径`,
properties: ['openDirectory'],
defaultPath: platformSettings.value[platform].exportPath
})
if (!result.canceled && result.filePaths?.length > 0) {
platformSettings.value[platform].exportPath = result.filePaths[0]
}
}
// 保存设置
function saveAllSettings() {
Object.keys(platformSettings.value).forEach(platformKey => {
const platform = platformKey as Platform
const platformConfig = platformSettings.value[platform]
savePlatformSettings(platform, platformConfig)
})
ElMessage({ message: '设置已保存', type: 'success' })
show.value = false
}
// 加载设置
function loadAllSettings() {
const settings = getSettings()
platformSettings.value = {
amazon: { ...settings.platforms.amazon },
rakuten: { ...settings.platforms.rakuten },
zebra: { ...settings.platforms.zebra }
}
}
// 重置单个平台设置
function resetPlatformSettings(platform: Platform) {
platformSettings.value[platform] = {
exportPath: ''
}
}
// 重置所有设置
function resetAllSettings() {
platforms.forEach(platform => {
resetPlatformSettings(platform.key)
})
}
onMounted(() => {
loadAllSettings()
})
</script>
<template>
<el-dialog
v-model="show"
title="应用设置"
width="480px"
:close-on-click-modal="false"
class="settings-dialog">
<div class="settings-content">
<!-- 平台选择标签 -->
<div class="platform-tabs">
<div
v-for="platform in platforms"
:key="platform.key"
:class="['platform-tab', { active: activeTab === platform.key }]"
@click="activeTab = platform.key"
:style="{ '--platform-color': platform.color }">
<span class="platform-icon">{{ platform.icon }}</span>
<span class="platform-name">{{ platform.name }}</span>
</div>
</div>
<!-- 当前平台设置 -->
<div class="setting-section">
<div class="section-title">
<span class="title-icon">📁</span>
<span>{{ platforms.find(p => p.key === activeTab)?.name }} 导出设置</span>
</div>
<div class="setting-item">
<div class="setting-label">默认导出路径</div>
<div class="setting-desc">设置 {{ platforms.find(p => p.key === activeTab)?.name }} Excel文件的默认保存位置</div>
<div class="path-input-group">
<el-input
v-model="platformSettings[activeTab].exportPath"
placeholder="留空时自动弹出保存对话框"
readonly
class="path-input">
<template #suffix>
<el-button
size="small"
type="primary"
@click="selectExportPath(activeTab)"
class="select-btn">
浏览
</el-button>
</template>
</el-input>
</div>
</div>
<div class="setting-actions">
<el-button
size="small"
@click="resetPlatformSettings(activeTab)">
重置此平台
</el-button>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="resetAllSettings">重置全部</el-button>
<el-button @click="show = false">取消</el-button>
<el-button type="primary" @click="saveAllSettings">保存设置</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped>
.settings-dialog :deep(.el-dialog__body) {
padding: 0 20px 20px 20px;
}
.settings-content {
max-height: 500px;
overflow-y: auto;
}
.platform-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
padding: 4px;
background: #F8F9FA;
border-radius: 8px;
}
.platform-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
background: transparent;
color: #606266;
font-size: 13px;
}
.platform-tab:hover {
background: rgba(255, 255, 255, 0.8);
color: var(--platform-color);
}
.platform-tab.active {
background: #fff;
color: var(--platform-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-weight: 500;
}
.platform-icon {
font-size: 16px;
}
.platform-name {
font-size: 12px;
}
.setting-section {
margin-bottom: 24px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #EBEEF5;
}
.title-icon {
font-size: 18px;
}
.setting-item {
margin-bottom: 20px;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.setting-info {
flex: 1;
}
.setting-label {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.setting-desc {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
.path-input-group {
margin-top: 8px;
}
.path-input {
width: 100%;
}
.path-input :deep(.el-input__wrapper) {
padding-right: 80px;
}
.select-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
height: 24px;
padding: 0 12px;
font-size: 12px;
}
.info-content {
background: #F8F9FA;
border-radius: 6px;
padding: 12px;
font-size: 13px;
color: #606266;
line-height: 1.5;
}
.info-content p {
margin: 0 0 6px 0;
}
.info-content p:last-child {
margin-bottom: 0;
}
.setting-actions {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #EBEEF5;
}
.settings-dialog :deep(.el-dialog__header) {
text-align: center;
padding-right: 40px; /* 为右侧关闭按钮留出空间 */
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>
<script lang="ts">
export default {
name: 'SettingsDialog'
}
</script>

View File

@@ -2,15 +2,18 @@
<div>
<div class="version-info" @click="autoCheck">v{{ version || '-' }}</div>
<el-dialog v-model="show" width="522px" :close-on-click-modal="false" align-center class="update-dialog" :title="stage === 'downloading' ? `正在更新 ${appName}` : '软件更新'">
<el-dialog v-model="show" width="522px" :close-on-click-modal="false" align-center class="update-dialog"
:title="stage === 'downloading' ? `正在更新 ${appName}` : '软件更新'">
<div v-if="stage === 'check'" class="update-content">
<div class="update-layout">
<div class="left-pane">
<img src="/icon/icon.png" class="app-icon app-icon-large" alt="App Icon" />
<img src="/icon/icon.png" class="app-icon app-icon-large" alt="App Icon"/>
</div>
<div class="right-pane">
<p class="announce">新版本的"{{ appName }}"已经发布</p>
<p class="desc">{{ appName }} {{ info.latestVersion }} 可供安装您现在的版本是 {{ version }}要现在安装吗</p>
<p class="desc">{{ appName }} {{ info.latestVersion }} 可供安装您现在的版本是 {{
version
}}要现在安装吗</p>
<div class="update-details form">
<h4>更新信息</h4>
@@ -20,7 +23,7 @@
class="notes-box"
:rows="6"
readonly
resize="none" />
resize="none"/>
</div>
<div class="update-actions row">
@@ -41,7 +44,7 @@
<div v-else-if="stage === 'downloading'" class="update-content">
<div class="download-main">
<div class="download-icon">
<img src="/icon/icon.png" class="app-icon" alt="App Icon" />
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
</div>
<div class="download-content">
<div class="download-info">
@@ -52,7 +55,7 @@
:percentage="prog.percentage"
:show-text="false"
:stroke-width="6"
color="#409EFF" />
color="#409EFF"/>
<div class="progress-details">
<span style="font-weight: 500">{{ prog.current }} / {{ prog.total }}</span>
<el-button size="small" @click="cancelDownload">取消</el-button>
@@ -64,7 +67,7 @@
<div v-else-if="stage === 'completed'" class="update-content">
<div class="update-header text-center">
<img src="/icon/icon.png" class="app-icon" alt="App Icon" />
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
<h3>更新完成</h3>
<p>更新文件已下载将在重启后自动应用</p>
</div>
@@ -77,7 +80,7 @@
:percentage="100"
:show-text="false"
:stroke-width="6"
color="#67C23A" />
color="#67C23A"/>
</div>
<div class="update-buttons">
@@ -90,9 +93,9 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { updateApi } from '../../api/update'
import {ref, computed, onMounted, onUnmounted} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {updateApi} from '../../api/update'
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
@@ -105,8 +108,8 @@ const show = computed({
type Stage = 'check' | 'downloading' | 'completed'
const stage = ref<Stage>('check')
const appName = ref('我了个电商')
const version = ref('2.0.0')
const prog = ref({ percentage: 0, current: '0 MB', total: '0 MB', speed: '' })
const version = ref('')
const prog = ref({percentage: 0, current: '0 MB', total: '0 MB', speed: ''})
const info = ref({
latestVersion: '2.4.8',
downloadUrl: '',
@@ -117,49 +120,48 @@ const info = ref({
async function autoCheck() {
try {
ElMessage({ message: '正在检查更新...', type: 'info' })
version.value = await (window as any).electronAPI.getJarVersion()
const checkRes: any = await updateApi.checkUpdate(version.value)
const result = checkRes?.data || checkRes
try {
version.value = await updateApi.getVersion()
} catch (error) {
console.error('获取版本失败:', error)
version.value = '2.0.0'
if (!result.needUpdate) {
ElMessage.info('当前已是最新版本')
return
}
info.value = {
currentVersion: version.value,
latestVersion: '2.4.9',
downloadUrl: 'https://qiniu.pxdj.tashowz.com/2025/09/becac13811214c909d11162d2ff2c863.asar',
currentVersion: result.currentVersion,
latestVersion: result.latestVersion,
downloadUrl: result.downloadUrl || '',
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 轻量级更新仅替换app.asar',
hasUpdate: true
}
show.value = true
stage.value = 'check'
ElMessage({ message: '发现新版本', type: 'success' })
ElMessage.success('发现新版本')
} catch (error) {
console.error('检查更新失败:', error)
ElMessage({ message: '检查更新失败', type: 'error' })
ElMessage.error('检查更新失败')
}
}
async function start() {
if (!info.value.downloadUrl) {
ElMessage({ message: '下载链接不可用', type: 'error' });
return;
ElMessage.error('下载链接不可用')
return
}
stage.value = 'downloading';
prog.value = { percentage: 0, current: '0 MB', total: '0 MB', speed: '' };
stage.value = 'downloading'
prog.value = {percentage: 0, current: '0 MB', total: '0 MB', speed: ''}
(window as any).electronAPI.onDownloadProgress((progress: any) => {
;(window as any).electronAPI.onDownloadProgress((progress: any) => {
prog.value = {
percentage: progress.percentage || 0,
current: progress.current || '0 MB',
total: progress.total || '0 MB',
speed: progress.speed || ''
};
});
}
})
try {
const response = await (window as any).electronAPI.downloadUpdate(info.value.downloadUrl)
@@ -167,14 +169,14 @@ async function start() {
if (response.success) {
stage.value = 'completed'
prog.value.percentage = 100
ElMessage({ message: '下载完成', type: 'success' })
ElMessage.success('下载完成')
} else {
ElMessage({ message: '下载失败: ' + (response.error || '未知错误'), type: 'error' })
ElMessage.error('下载失败: ' + (response.error || '未知错误'))
stage.value = 'check'
}
} catch (error) {
console.error('下载失败:', error)
ElMessage({ message: '下载失败', type: 'error' })
ElMessage.error('下载失败')
stage.value = 'check'
}
}
@@ -195,37 +197,26 @@ async function cancelDownload() {
async function installUpdate() {
try {
await ElMessageBox.confirm(
'安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?',
'确认安装',
{
confirmButtonText: '立即安装',
cancelButtonText: '取消',
type: 'warning'
}
'安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?',
'确认安装',
{
confirmButtonText: '立即安装',
cancelButtonText: '取消',
type: 'warning'
}
)
const response = await (window as any).electronAPI.installUpdate()
if (response.success) {
ElMessage({ message: '应用即将重启', type: 'success' })
ElMessage.success('应用即将重启')
setTimeout(() => show.value = false, 1000)
} else {
ElMessage({ message: '重启失败: ' + (response.error || '未知错误'), type: 'error' })
stage.value = 'check'
}
} catch (error) {
if (error !== 'cancel') {
console.error('安装失败:', error)
ElMessage({ message: '安装失败', type: 'error' })
}
if (error !== 'cancel') ElMessage.error('安装失败')
}
}
onMounted(async () => {
try {
version.value = await updateApi.getVersion()
} catch (error) {
console.error('获取版本失败:', error)
}
version.value = await (window as any).electronAPI.getJarVersion()
})
onUnmounted(() => {
@@ -238,7 +229,7 @@ onUnmounted(() => {
position: fixed;
right: 10px;
bottom: 10px;
background: rgba(255,255,255,0.9);
background: rgba(255, 255, 255, 0.9);
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
@@ -273,11 +264,38 @@ onUnmounted(() => {
margin-bottom: 5px;
}
.left-pane { display: flex; flex-direction: column; align-items: flex-start; }
.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; }
.right-pane .desc { font-size: 13px; color: #6b7280; line-height: 1.6; margin: 0; word-break: break-word; }
.left-pane {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.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;
}
.right-pane .desc {
font-size: 13px;
color: #6b7280;
line-height: 1.6;
margin: 0;
word-break: break-word;
}
.update-header {
display: flex;
@@ -324,8 +342,13 @@ onUnmounted(() => {
margin: 12px 0 8px 0;
}
.update-details.form { max-height: none; }
.notes-box :deep(textarea.el-textarea__inner) { white-space: pre-wrap; }
.update-details.form {
max-height: none;
}
.notes-box :deep(textarea.el-textarea__inner) {
white-space: pre-wrap;
}
.update-details h4 {
font-size: 14px;
@@ -347,10 +370,24 @@ onUnmounted(() => {
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; }
.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;

View File

@@ -49,9 +49,7 @@ const selectedFileName = ref('')
const pendingFile = ref<File | null>(null)
const region = ref('JP')
const regionOptions = [
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' },
{ label: '美国 (USA)', value: 'US', flag: '🇺🇸' },
{ label: '中国 (China)', value: 'CN', flag: '🇨🇳' },
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' }
]
// 获取数据筛选:查询日期
const dateRange = ref<string[] | null>(null)
@@ -432,7 +430,7 @@ onMounted(loadLatest)
<div class="title">网站地区</div>
</div>
<div class="desc">请选择目标网站地区日本区</div>
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%">
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%" disabled>
<el-option v-for="opt in regionOptions" :key="opt.value" :label="opt.label" :value="opt.value">
<span style="margin-right:6px">{{ opt.flag }}</span>{{ opt.label }}
</el-option>
@@ -602,14 +600,14 @@ onMounted(loadLatest)
.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; margin-bottom: 8px; text-align: left; }
.steps-title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
/* 卡片式步骤,与示例一致 */
.steps-flow { position: relative; }
.steps-flow:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
.flow-item { position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 10px; padding: 8px 0; }
.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 + .flow-item { border-top: 1px dashed #ebeef5; }
.flow-item .step-index { position: static; width: 24px; height: 24px; line-height: 24px; 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; }
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }

View File

@@ -538,12 +538,12 @@ export default {
.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-steps { position: relative; }
.step { display: grid; grid-template-columns: 24px 1fr; gap: 10px; position: relative; padding: 8px 0; }
.step { display: grid; grid-template-columns: 22px 1fr; gap: 10px; position: relative; padding: 8px 0; }
.step + .step { border-top: 1px dashed #ebeef5; }
.step-index { width: 24px; height: 24px; 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-title { font-size: 13px; color: #606266; margin-bottom: 6px; font-weight: 600; text-align: left; }
.aside-steps:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
.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; }
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
.step-accounts { position: relative; }

View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.tashow.erp</groupId>
<artifactId>erp_client_sb</artifactId>
<version>2.4.7</version>
<version>2.4.8</version>
<name>erp_client_sb</name>
<description>erp客户端</description>
<properties>

View File

@@ -13,23 +13,15 @@ public class ErpClientSbApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(ErpClientSbApplication.class, args);
try {
ErrorReporter errorReporter = applicationContext.getBean(ErrorReporter.class);
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
log.error("捕获到未处理异常: " + ex.getMessage(), ex);
errorReporter.reportSystemError("未捕获异常: " + thread.getName(), (Exception) ex);
});
log.info("Started Success");
} catch (Exception e) {
log.warn("未设置 ErrorReporter继续启动: {}", e.getMessage());
}
ErrorReporter errorReporter = applicationContext.getBean(ErrorReporter.class);
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
log.error("捕获到未处理异常: " + ex.getMessage(), ex);
errorReporter.reportSystemError("捕获异常: " + thread.getName(), (Exception) ex);
});
log.info("Started Success");
ResourcePreloader.init();
ResourcePreloader.preloadErpDashboard();
ResourcePreloader.executePreloading();
try {
ResourcePreloader.init();
ResourcePreloader.preloadErpDashboard();
ResourcePreloader.executePreloading();
} catch (Throwable t) {
log.warn("资源预加载失败: {}", t.getMessage());
}
}
}

View File

@@ -21,6 +21,9 @@ public class AmazonProductEntity {
@Column
private String asin;
@Column(name = "region", length = 10)
private String region; // 地区代码(JP/US)
@Column(name = "price")
private String price;

View File

@@ -21,10 +21,10 @@ import java.util.Optional;
public interface AmazonProductRepository extends JpaRepository<AmazonProductEntity, Long> {
/**
* 根据ASIN查找产品取最新的一条
* 根据ASIN和地区查找产品(取最新的一条)
*/
@Query(value = "SELECT * FROM amazon_products WHERE asin = :asin ORDER BY created_at DESC LIMIT 1", nativeQuery = true)
Optional<AmazonProductEntity> findByAsin(@Param("asin") String asin);
@Query(value = "SELECT * FROM amazon_products WHERE asin = :asin AND region = :region ORDER BY created_at DESC LIMIT 1", nativeQuery = true)
Optional<AmazonProductEntity> findByAsinAndRegion(@Param("asin") String asin, @Param("region") String region);
/**
* 根据会话ID查找产品分页

View File

@@ -113,7 +113,7 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
if (asin == null || asin.trim().isEmpty()) continue;
String cleanAsin = asin.replaceAll("[^a-zA-Z0-9]", "");
Optional<AmazonProductEntity> cached = amazonProductRepository.findByAsin(cleanAsin);
Optional<AmazonProductEntity> cached = amazonProductRepository.findByAsinAndRegion(cleanAsin, region);
if (cached.isPresent() && !isEmpty(cached.get().getPrice()) && !isEmpty(cached.get().getSeller())) {
AmazonProductEntity entity = cached.get();
entity.setSessionId(sessionId);
@@ -131,6 +131,7 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
}
AmazonProductEntity entity = resultCache.getOrDefault(cleanAsin, new AmazonProductEntity());
entity.setAsin(cleanAsin);
entity.setRegion(region);
entity.setSessionId(sessionId);
entity.setUpdatedAt(LocalDateTime.now());
amazonProductRepository.save(entity);
@@ -160,56 +161,27 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
* 根据地区配置Site
*/
private Site configureSiteForRegion(String region) {
Site baseSite = Site.me()
boolean isUS = "US".equals(region);
return Site.me()
.setRetryTimes(3)
.setSleepTime(3000 + random.nextInt(3000))
.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 Edg/127.0.0.0")
.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
.addHeader("accept-encoding", "gzip, deflate, br, zstd")
.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("upgrade-insecure-requests", "1")
.addHeader("sec-ch-ua", "\"Not)A;Brand\";v=\"99\", \"Microsoft Edge\";v=\"127\", \"Chromium\";v=\"127\"")
.addHeader("sec-ch-ua-mobile", "?0")
.addHeader("sec-ch-ua-platform", "\"Windows\"")
.addHeader("sec-ch-ua-platform-version", "\"10.0.0\"")
.addHeader("sec-ch-device-memory", "8")
.addHeader("sec-ch-viewport-width", "1400")
.addHeader("device-memory", "8")
.addHeader("viewport-width", "1400")
.addHeader("dpr", "0.9")
.addHeader("downlink", "10")
.addHeader("ect", "4g")
.addHeader("rtt", "150")
.addHeader("referer", isUS ? "https://www.amazon.com/" : "https://www.amazon.co.jp/")
.addHeader("sec-fetch-site", "none")
.addHeader("sec-fetch-mode", "navigate")
.addHeader("sec-fetch-user", "?1")
.addHeader("sec-fetch-dest", "document");
if ("US".equals(region)) {
// 美区配置
baseSite.addHeader("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
.addHeader("referer", "https://www.amazon.com/")
.addCookie("i18n-prefs", "USD")
.addCookie("lc-main", "en_US")
.addCookie("session-id", "134-6097934-2082600")
.addCookie("session-id-time", "2082787201l")
.addCookie("ubid-main", "132-7547587-3056927")
.addCookie("skin", "noskin")
.addCookie("csm-hit", "tb:s-6ZB8JV440R1VZ54PSE5W|1759200029304&t:1759200030204&adb:adblk_no");
} else {
// 日区配置
baseSite.addHeader("accept-language", "ja,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
.addHeader("referer", "https://www.amazon.co.jp/")
.addCookie("i18n-prefs", "JPY")
.addCookie("lc-acbjp", "zh_CN")
.addCookie("session-id", "358-1261309-0483141")
.addCookie("session-id-time", "2082787201l")
.addCookie("ubid-acbjp", "357-8224002-9668932")
.addCookie("skin", "noskin");
}
return baseSite;
.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");
}
}

View File

@@ -26,7 +26,7 @@ public class VersionController extends BaseController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String VERSION_REDIS_KEY = "erp:client:version";
private static final String DOWNLOAD_URL_REDIS_KEY = "erp:client:download:url";
private static final String DOWNLOAD_URL_REDIS_KEY = "erp:client:url";
/**
* 检查版本更新
@@ -85,23 +85,12 @@ public class VersionController extends BaseController {
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
@RequestParam("downloadUrl") String downloadUrl) {
try {
if (StringUtils.isEmpty(version)) {
return AjaxResult.error("版本号不能为空");
}
if (StringUtils.isEmpty(downloadUrl)) {
return AjaxResult.error("下载链接不能为空");
}
// 更新Redis中的版本信息和下载链接
redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version);
redisTemplate.opsForValue().set(DOWNLOAD_URL_REDIS_KEY, downloadUrl);
Map<String, Object> result = new HashMap<>();
result.put("version", version);
result.put("downloadUrl", downloadUrl);
result.put("updateTime", System.currentTimeMillis());
return AjaxResult.success("版本信息更新成功", result);
} catch (Exception e) {
return AjaxResult.error("版本信息更新失败: " + e.getMessage());

View File

@@ -42,9 +42,6 @@ public class FileController {
@PostMapping("/uploads")
public AjaxResult uploadFiles(@RequestParam("files") List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
return AjaxResult.error("没有选择文件");
}
List<FileDto> fileDtoS = new ArrayList<>();
for (MultipartFile file : files) {

View File

@@ -6,7 +6,6 @@
<el-button type="success" icon="el-icon-upload" size="mini" @click="handleUpload" v-hasPermi="['system:version:upload']">上传新版本</el-button>
</el-form-item>
</el-form>
<!-- 版本信息卡片 -->
<el-row class="mb8">
<el-col s>
@@ -44,14 +43,14 @@
ref="upload"
action="#"
:limit="1"
accept=".exe"
accept=".asar"
:on-exceed="handleExceed"
:file-list="fileList"
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove">
<el-button slot="trigger" size="small" type="primary">选择文件</el-button>
<div slot="tip" class="el-upload__tip">只能上传exe文件且不超过800MB</div>
<div slot="tip" class="el-upload__tip">只能上传asar文件且不超过800MB</div>
</el-upload>
</el-form-item>
</el-form>
@@ -169,8 +168,8 @@ export default {
return;
}
if (!this.uploadForm.file.name.endsWith('.exe')) {
this.$modal.msgError("只支持上传.exe文件");
if (!this.uploadForm.file.name.endsWith('.asar')) {
this.$modal.msgError("只支持上传.asar文件");
return;
}