diff --git a/README.md b/README.md
index c04c646d..a789f5d3 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@
想快速体验完整商城,不需要分别部署后端、PC、商家端、运营端、IM、H5、MySQL 和 Redis。安装 Docker Desktop 后,直接执行下面脚本,按提示输入 IP/域名、端口、密码和数据目录即可启动单镜像单容器环境。
+数据库口径:常规部署和项目技术栈仍以 MySQL 为准;All-In-One 镜像内置 MariaDB 只是单容器本地体验中的 MySQL 协议兼容实现,不代表生产部署或常规部署已经切换为 MariaDB。
+
macOS / Linux:
```bash
@@ -26,7 +28,7 @@ Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\install-lilishop.ps1
```
-默认镜像:`ccr.ccs.tencentyun.com/lilishop/lilishop-all-in-one:lite`。启动后可访问买家 PC、商家端、运营端、IM Web、uniapp H5 和后端 API。更多说明见 [docker/all-in-one 文档](https://gitee.com/beijing_hongye_huicheng/docker/tree/allinone-lite-mysql-redis/all-in-one)。
+默认镜像:`ccr.ccs.tencentyun.com/lilishop/lilishop-all-in-one:lite`。启动后可访问买家 PC、商家端、运营端、IM Web、uniapp H5,统一后端由 All-In-One 的同域 `/api/` 入口代理。更多说明见 [docker/all-in-one 文档](https://gitee.com/beijing_hongye_huicheng/docker/tree/allinone-lite-mysql-redis/all-in-one)。
LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开源商城系统,支持 B2B2C 多商户商城、小程序商城、微服务商城、直播电商、分销返佣、秒杀活动、Docker 私有化部署。
@@ -80,7 +82,7 @@ Lilishop 由 4 个独立仓库组成,均同步托管于 GitHub 与 Gitee:
- **全端覆盖**: 一套代码库支持PC、H5、小程序、APP,降低开发和维护成本。
- **商家入驻**: 支持多商家入驻,构建平台化电商生态。
-- **分布式架构**: 后端API服务化,支持独立部署和弹性伸缩。
+- **All-In-One 后端**: 当前分支推荐使用 `lilishop-all` 统一后端入口,降低本地体验和私有化部署门槛。
- **前后端分离**: 清晰的职责划分,便于团队协作和独立开发。
- **容器化支持**: 提供Docker镜像和docker-compose配置,实现一键部署。
- **功能完善**: 涵盖客户、订单、商品、促销、店铺、运营、统计等完整电商业务模块。
diff --git a/buyer/public/config.js b/buyer/public/config.js
index 2272ff76..37fa5df8 100644
--- a/buyer/public/config.js
+++ b/buyer/public/config.js
@@ -3,15 +3,15 @@ var BASE = {
* @description api请求基础路径
*/
API_DEV: {
- common: "https://common-api.pickmall.cn",
- buyer: "https://buyer-api.pickmall.cn",
- seller: "https://store-api.pickmall.cn",
- manager: "https://admin-api.pickmall.cn"
+ common: "/api",
+ buyer: "/api",
+ seller: "/api",
+ manager: "/api"
},
API_PROD: {
- common: "https://common-api.pickmall.cn",
- buyer: "https://buyer-api.pickmall.cn",
- seller: "https://store-api.pickmall.cn",
- manager: "https://admin-api.pickmall.cn"
+ common: "/api",
+ buyer: "/api",
+ seller: "/api",
+ manager: "/api"
},
};
diff --git a/im/.env b/im/.env
index 070b82b5..aaf0b890 100644
--- a/im/.env
+++ b/im/.env
@@ -1,10 +1,10 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
-VUE_APP_API_BASE_URL=https://im-api.pickmall.cn
-VUE_APP_WEB_SOCKET_URL=wss://im-api.pickmall.cn/lili/webSocket
-VUE_APP_COMMON=https://common-api.pickmall.cn
-VUE_APP_BUYER=https://buyer-api.pickmall.cn
-VUE_APP_SELLER=https://store-api.pickmall.cn
+VUE_APP_API_BASE_URL=/api
+VUE_APP_WEB_SOCKET_URL=/lili/webSocket
+VUE_APP_COMMON=/api
+VUE_APP_BUYER=/api
+VUE_APP_SELLER=/api
VUE_APP_WEBSITE_NAME="LiLi IM"
VUE_APP_PC_URL=https://pc-b2b2c.pickmall.cn/
-VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/
\ No newline at end of file
+VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/
diff --git a/im/.env.development b/im/.env.development
index 2b51a5e6..7e9445f3 100644
--- a/im/.env.development
+++ b/im/.env.development
@@ -1,10 +1,10 @@
NODE_ENV=development
VUE_APP_PREVIEW=false
-VUE_APP_API_BASE_URL=https://im-api.pickmall.cn
-VUE_APP_WEB_SOCKET_URL=wss://im-api.pickmall.cn/lili/webSocket
-VUE_APP_COMMON=https://common-api.pickmall.cn
-VUE_APP_BUYER=https://buyer-api.pickmall.cn
-VUE_APP_SELLER=https://store-api.pickmall.cn
+VUE_APP_API_BASE_URL=/api
+VUE_APP_WEB_SOCKET_URL=/lili/webSocket
+VUE_APP_COMMON=/api
+VUE_APP_BUYER=/api
+VUE_APP_SELLER=/api
VUE_APP_WEBSITE_NAME="LiLi IM"
VUE_APP_PC_URL=https://pc-b2b2c.pickmall.cn/
-VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/
\ No newline at end of file
+VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/
diff --git a/im/README.md b/im/README.md
index 6ac928fc..f13e9600 100644
--- a/im/README.md
+++ b/im/README.md
@@ -43,7 +43,7 @@
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
- server_name im-api.pickmall.cn;
+ server_name shop.example.com;
location / {
proxy_pass http://127.0.0.1:8088;
}
@@ -68,4 +68,4 @@
try_files $uri $uri/ /index.html;
root /home/im/im/dist;
}
-````
\ No newline at end of file
+````
diff --git a/im/src/config/config.js b/im/src/config/config.js
index fe283a94..f42cf217 100644
--- a/im/src/config/config.js
+++ b/im/src/config/config.js
@@ -22,13 +22,24 @@ const normalizeUrl = (url) => {
return url.endsWith("/") ? url : `${url}/`;
};
+const normalizeWsUrl = (url) => {
+ if (!url || /^wss?:\/\//.test(url)) {
+ return url || "";
+ }
+ if (typeof window !== "undefined" && url.startsWith("/")) {
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+ return `${protocol}//${window.location.host}${url}`;
+ }
+ return url;
+};
+
export default {
// 网站名称
WEBSITE_NAME: process.env.VUE_APP_WEBSITE_NAME || "LiLi IM",
// 默认请求IM的API
BASE_API_URL: runtimeApi || process.env.VUE_APP_API_BASE_URL || "",
// 默认请求的WS
- BASE_WS_URL: runtimeBase.IM_WS_URL || process.env.VUE_APP_WEB_SOCKET_URL || "",
+ BASE_WS_URL: normalizeWsUrl(runtimeBase.IM_WS_URL || process.env.VUE_APP_WEB_SOCKET_URL || ""),
// 默认请求公有接口相关 API
BASE_COMMON: apiProd.common || apiDev.common || process.env.VUE_APP_COMMON || "",
// 默认请求用户相关API
diff --git a/manager/public/config.js b/manager/public/config.js
index 9f51858a..ed8f21ce 100644
--- a/manager/public/config.js
+++ b/manager/public/config.js
@@ -3,16 +3,16 @@ var BASE = {
* @description api请求基础路径
*/
API_DEV: {
- common: "https://common-api.pickmall.cn",
- buyer: "https://buyer-api.pickmall.cn",
- seller: "https://store-api.pickmall.cn",
- manager: "https://admin-api.pickmall.cn",
+ common: "/api",
+ buyer: "/api",
+ seller: "/api",
+ manager: "/api",
},
API_PROD: {
- common: "https://common-api.pickmall.cn",
- buyer: "https://buyer-api.pickmall.cn",
- seller: "https://store-api.pickmall.cn",
- manager: "https://admin-api.pickmall.cn"
+ common: "/api",
+ buyer: "/api",
+ seller: "/api",
+ manager: "/api"
},
/**
* @description // 跳转买家端地址 pc端
diff --git a/manager/src/views/order/after-order/afterSaleOrderDetail.vue b/manager/src/views/order/after-order/afterSaleOrderDetail.vue
index eea1eb73..91b9be4e 100644
--- a/manager/src/views/order/after-order/afterSaleOrderDetail.vue
+++ b/manager/src/views/order/after-order/afterSaleOrderDetail.vue
@@ -272,12 +272,12 @@
@@ -312,13 +312,13 @@
- 物流公司:
-
-
{{ afterSaleInfo.mlogisticsName }}
+ {{ buyerReturnLogisticsName }}
- 物流单号:
-
-
{{ afterSaleInfo.mlogisticsNo }}
+ {{ buyerReturnLogisticsNo }}
@@ -410,7 +410,24 @@ export default {
],
};
},
+ computed: {
+ buyerReturnLogisticsName() {
+ return this.pickAfterSaleField("mlogisticsName", "mLogisticsName", "MLogisticsName", "m_logistics_name");
+ },
+ buyerReturnLogisticsNo() {
+ return this.pickAfterSaleField("mlogisticsNo", "mLogisticsNo", "MLogisticsNo", "m_logistics_no");
+ },
+ },
methods: {
+ pickAfterSaleField(...fields) {
+ for (const field of fields) {
+ const value = this.afterSaleInfo && this.afterSaleInfo[field];
+ if (value !== undefined && value !== null && value !== "") {
+ return value;
+ }
+ }
+ return "";
+ },
// 获取售后详情
getDetail() {
this.loading = true;
diff --git a/seller/public/config.js b/seller/public/config.js
index 87161bcb..57ff2cc3 100644
--- a/seller/public/config.js
+++ b/seller/public/config.js
@@ -3,16 +3,16 @@ var BASE = {
* @description api请求基础路径
*/
API_DEV: {
- common: "https://common-api.pickmall.cn",
- buyer: "https://buyer-api.pickmall.cn",
- seller: "https://store-api.pickmall.cn",
- manager: "https://admin-api.pickmall.cn",
+ common: "/api",
+ buyer: "/api",
+ seller: "/api",
+ manager: "/api",
},
API_PROD: {
- common: "https://common-api.pickmall.cn",
- buyer: "https://buyer-api.pickmall.cn",
- seller: "https://store-api.pickmall.cn",
- manager: "https://admin-api.pickmall.cn",
+ common: "/api",
+ buyer: "/api",
+ seller: "/api",
+ manager: "/api",
},
/**
* @description // 跳转买家端地址 pc端
diff --git a/seller/src/api/order.contract.test.mjs b/seller/src/api/order.contract.test.mjs
new file mode 100644
index 00000000..80144eb5
--- /dev/null
+++ b/seller/src/api/order.contract.test.mjs
@@ -0,0 +1,19 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import test from 'node:test';
+
+const orderApi = readFileSync(new URL('./order.js', import.meta.url), 'utf8');
+const afterSaleDetail = readFileSync(new URL('../views/order/after-order/reurnGoodsOrderDetail.vue', import.meta.url), 'utf8');
+const managerAfterSaleDetail = readFileSync(new URL('../../../manager/src/views/order/after-order/afterSaleOrderDetail.vue', import.meta.url), 'utf8');
+
+test('complaint reply posts to the exact seller-api communication route', () => {
+ assert.match(orderApi, /postRequest\(`\/order\/complain\/communication`, params\)/);
+ assert.doesNotMatch(orderApi, /\/order\/complain\/communication\//);
+});
+
+test('after-sale detail displays buyer return logistics from backend JSON fields', () => {
+ assert.match(afterSaleDetail, /buyerReturnLogisticsName/);
+ assert.match(afterSaleDetail, /buyerReturnLogisticsNo/);
+ assert.match(managerAfterSaleDetail, /buyerReturnLogisticsName/);
+ assert.match(managerAfterSaleDetail, /buyerReturnLogisticsNo/);
+});
diff --git a/seller/src/api/order.js b/seller/src/api/order.js
index 601e18c4..85f16449 100644
--- a/seller/src/api/order.js
+++ b/seller/src/api/order.js
@@ -73,7 +73,7 @@ export const getComplainDetail = id => {
//添加交易投诉对话
export const addOrderComplaint = params => {
- return postRequest(`/order/complain/communication/`, params);
+ return postRequest(`/order/complain/communication`, params);
};
//添加交易投诉对话
diff --git a/seller/src/api/shops.contract.test.mjs b/seller/src/api/shops.contract.test.mjs
new file mode 100644
index 00000000..45a28976
--- /dev/null
+++ b/seller/src/api/shops.contract.test.mjs
@@ -0,0 +1,11 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import test from 'node:test';
+
+const shopsApi = readFileSync(new URL('./shops.js', import.meta.url), 'utf8');
+
+test('store pickup address list and create use backend route without trailing slash', () => {
+ assert.match(shopsApi, /getRequest\(`\/member\/storeAddress`, params\)/);
+ assert.match(shopsApi, /postRequest\(`\/member\/storeAddress`, params\)/);
+ assert.doesNotMatch(shopsApi, /\/member\/storeAddress\/`/);
+});
diff --git a/seller/src/api/shops.js b/seller/src/api/shops.js
index c5c2f695..5efb8d62 100644
--- a/seller/src/api/shops.js
+++ b/seller/src/api/shops.js
@@ -71,7 +71,7 @@ export const logisticsUnChecked = (id, params) => {
}
// 获取商家自提点
export const getShopAddress = (id, params) => {
- return getRequest(`/member/storeAddress/`, params)
+ return getRequest(`/member/storeAddress`, params)
}
// 修改商家自提点
@@ -81,7 +81,7 @@ export const editShopAddress = (id, params) => {
// 添加商品自提点
export const addShopAddress = (params) => {
- return postRequest(`/member/storeAddress/`, params)
+ return postRequest(`/member/storeAddress`, params)
}
// 添加商品自提点
@@ -140,4 +140,3 @@ export const editChecked = (logisticsId,params) => {
return putRequest(`/other/logistics/${logisticsId}/updateStoreLogistics`,params)
}
-
diff --git a/seller/src/libs/axios.contract.test.mjs b/seller/src/libs/axios.contract.test.mjs
new file mode 100644
index 00000000..1c0cfa2e
--- /dev/null
+++ b/seller/src/libs/axios.contract.test.mjs
@@ -0,0 +1,12 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { test } from "node:test";
+
+const source = readFileSync("seller/src/libs/axios.js", "utf8");
+
+test("seller token refresh waits on the refresh request promise", () => {
+ assert.match(source, /let refreshPromise = null;/);
+ assert.match(source, /refreshPromise = handleRefreshToken\(oldRefreshToken\)/);
+ assert.match(source, /return refreshPromise;/);
+ assert.doesNotMatch(source, /setInterval\(/, "refresh should not depend on polling shared state");
+});
diff --git a/seller/src/libs/axios.js b/seller/src/libs/axios.js
index a61bcf61..96492122 100644
--- a/seller/src/libs/axios.js
+++ b/seller/src/libs/axios.js
@@ -136,45 +136,29 @@ service.interceptors.response.use(
// 防抖闭包来一波
function getTokenDebounce() {
- let lock = false;
- let success = false;
+ let refreshPromise = null;
return function() {
- if (!lock) {
- lock = true;
+ if (!refreshPromise) {
let oldRefreshToken = getStore("refreshToken");
- handleRefreshToken(oldRefreshToken)
- .then(res => {
+ refreshPromise = handleRefreshToken(oldRefreshToken)
+ .then((res) => {
if (res.success) {
let { accessToken, refreshToken } = res.result;
setStore("accessToken", accessToken);
setStore("refreshToken", refreshToken);
-
- success = true;
- lock = false;
+ return "success";
} else {
- success = false;
- lock = false;
- // router.push('/login')
+ return "fail";
}
})
- .catch(err => {
- success = false;
- lock = false;
+ .catch(() => {
+ return "fail";
+ })
+ .finally(() => {
+ refreshPromise = null;
});
}
- return new Promise(resolve => {
- // 一直看lock,直到请求失败或者成功
- const timer = setInterval(() => {
- if (!lock) {
- clearInterval(timer);
- if (success) {
- resolve("success");
- } else {
- resolve("fail");
- }
- }
- }, 500); // 轮询时间间隔
- });
+ return refreshPromise;
};
}
@@ -410,4 +394,3 @@ export const postRequestWithNoToken = (url, params) => {
data: params
});
};
-
diff --git a/seller/src/utils/file-url.contract.test.mjs b/seller/src/utils/file-url.contract.test.mjs
new file mode 100644
index 00000000..7c2ba962
--- /dev/null
+++ b/seller/src/utils/file-url.contract.test.mjs
@@ -0,0 +1,28 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { test } from "node:test";
+
+const source = readFileSync("seller/src/utils/file-url.js", "utf8");
+
+test("file url normalizer targets local storage placeholder paths only", () => {
+ assert.match(source, /LOCAL_FILE_PREFIX\s*=\s*["']\/files["']/, "should declare the local file placeholder prefix");
+ assert.match(source, /startsWith\(LOCAL_FILE_PREFIX \+ ["']\/["']\)/, "should only rewrite /files/... urls");
+ assert.match(source, /\^\(https\?:\)\?\\\/\\\//, "should keep absolute http urls");
+ assert.match(source, /data\|blob\|mailto\|tel/, "should keep non-http browser urls");
+});
+
+test("file url normalizer uses common api origin for resource records", () => {
+ assert.match(source, /function normalizeFileUrl\(url,\s*apiBase\)/, "should accept api origin explicitly");
+ assert.match(source, /String\(apiBase \|\| ["']["']\)\.replace\(\/\\\/\+\$\/,\s*["']["']\)/, "should trim trailing slashes from api origin");
+ assert.match(source, /return `\$\{base\}\$\{trimmed\}`/, "should prepend api origin to local file paths");
+ assert.match(source, /function normalizeFileRecords\(records,\s*apiBase\)/, "should normalize resource record lists");
+ assert.match(source, /url:\s*normalizeFileUrl\(item\.url,\s*apiBase\)/, "should normalize each record url");
+});
+
+test("file url normalizer parses selected resource values into objects", () => {
+ assert.match(source, /function parseSelectedFileValue\(value\)/, "should expose a parser for checkbox values");
+ assert.match(source, /value\.indexOf\(["'],["']\)/, "should split only at the id/url separator");
+ assert.match(source, /id:\s*value\.slice\(0,\s*separatorIndex\)/, "should preserve selected resource id");
+ assert.match(source, /url:\s*value\.slice\(separatorIndex \+ 1\)/, "should preserve the whole selected url");
+ assert.match(source, /function normalizeSelectedFileValues\(values\)/, "should expose selected value list normalization");
+});
diff --git a/seller/src/utils/file-url.js b/seller/src/utils/file-url.js
new file mode 100644
index 00000000..2d50d91d
--- /dev/null
+++ b/seller/src/utils/file-url.js
@@ -0,0 +1,80 @@
+const LOCAL_FILE_PREFIX = "/files";
+
+export function normalizeFileUrl(url, apiBase) {
+ if (!url || typeof url !== "string") {
+ return url;
+ }
+
+ const trimmed = url.trim();
+ if (
+ /^(https?:)?\/\//i.test(trimmed) ||
+ /^(data|blob|mailto|tel):/i.test(trimmed)
+ ) {
+ return url;
+ }
+
+ if (
+ trimmed !== LOCAL_FILE_PREFIX &&
+ !trimmed.startsWith(LOCAL_FILE_PREFIX + "/")
+ ) {
+ return url;
+ }
+
+ const base = String(apiBase || "").replace(/\/+$/, "");
+ if (!base) {
+ return url;
+ }
+
+ return `${base}${trimmed}`;
+}
+
+export function normalizeFileRecords(records, apiBase) {
+ if (!Array.isArray(records)) {
+ return records;
+ }
+
+ return records.map((item) => {
+ if (!item) {
+ return item;
+ }
+ return {
+ ...item,
+ url: normalizeFileUrl(item.url, apiBase),
+ };
+ });
+}
+
+export function parseSelectedFileValue(value) {
+ if (!value) {
+ return null;
+ }
+ if (typeof value === "object") {
+ return value.url ? value : null;
+ }
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const separatorIndex = value.indexOf(",");
+ if (separatorIndex < 0) {
+ return {
+ id: "",
+ url: value,
+ };
+ }
+
+ return {
+ id: value.slice(0, separatorIndex),
+ url: value.slice(separatorIndex + 1),
+ };
+}
+
+export function normalizeSelectedFileValues(values) {
+ if (!Array.isArray(values)) {
+ return [];
+ }
+
+ return values
+ .map((value) => parseSelectedFileValue(value))
+ .filter((item) => item && item.url);
+}
diff --git a/seller/src/views/goods/goods-seller/goodsOperationSec.contract.test.mjs b/seller/src/views/goods/goods-seller/goodsOperationSec.contract.test.mjs
new file mode 100644
index 00000000..b12461c2
--- /dev/null
+++ b/seller/src/views/goods/goods-seller/goodsOperationSec.contract.test.mjs
@@ -0,0 +1,25 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import test from 'node:test';
+
+const component = readFileSync(new URL('./goodsOperationSec.vue', import.meta.url), 'utf8');
+
+test('resource-library image import stores only image urls in goodsGalleryFiles', () => {
+ assert.match(component, /normalizeImageUrl/);
+ assert.match(component, /appendSelectedImages/);
+ assert.doesNotMatch(component, /baseInfoForm\[this\.selectedFormBtnName\] = \[\.\.\.this\.baseInfoForm\[this\.selectedFormBtnName\], \.\.\.this\.selectedImage\]/);
+});
+
+test('goods submit filters invalid gallery entries before building goodsGalleryList', () => {
+ assert.match(component, /filter\(Boolean\)/);
+ assert.match(component, /submit\.goodsGalleryFiles = submit\.goodsGalleryFiles/);
+});
+
+test('category parameter validation reset guards missing clearValidate method', () => {
+ assert.match(component, /typeof this\.\$refs\.baseInfoForm\.clearValidate === ["']function["']/);
+});
+
+test('direct goods uploads always send a fallback directory path', () => {
+ assert.match(component, /uploadDirectoryData:\s*\{\s*directoryPath:\s*["']default["']/);
+ assert.match(component, /:data=["']uploadDirectoryData["']/);
+});
diff --git a/seller/src/views/goods/goods-seller/goodsOperationSec.vue b/seller/src/views/goods/goods-seller/goodsOperationSec.vue
index de1bf0ec..deb4c737 100644
--- a/seller/src/views/goods/goods-seller/goodsOperationSec.vue
+++ b/seller/src/views/goods/goods-seller/goodsOperationSec.vue
@@ -156,7 +156,8 @@
-
- this.normalizeImageUrl(image))
+ .filter(Boolean);
+ return [...current, ...selected];
+ },
// 局部刷新
refresh(v) {
if (v == 'template') {
@@ -1272,7 +1293,7 @@ export default {
// 确保表单验证能正确初始化
this.$nextTick(() => {
- if (this.$refs.baseInfoForm) {
+ if (this.$refs.baseInfoForm && typeof this.$refs.baseInfoForm.clearValidate === "function") {
this.$refs.baseInfoForm.clearValidate();
}
});
@@ -2035,6 +2056,14 @@ export default {
this.$Message.error("请上传商品图片");
return;
}
+ submit.goodsGalleryFiles = submit.goodsGalleryFiles
+ .map((image) => this.normalizeImageUrl(image))
+ .filter(Boolean);
+ if (submit.goodsGalleryFiles.length <= 0) {
+ this.submitLoading = false;
+ this.$Message.error("请上传商品图片");
+ return;
+ }
if (submit.templateId === "") submit.templateId = 0;
let flag = false;
let paramValue = "";
diff --git a/seller/src/views/lili-components/editor/upload-image.contract.test.mjs b/seller/src/views/lili-components/editor/upload-image.contract.test.mjs
new file mode 100644
index 00000000..809f8710
--- /dev/null
+++ b/seller/src/views/lili-components/editor/upload-image.contract.test.mjs
@@ -0,0 +1,11 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { test } from "node:test";
+
+const source = readFileSync("seller/src/views/lili-components/editor/upload-image.vue", "utf8");
+
+test("editor resource import is confirmed once from selected resource objects", () => {
+ assert.doesNotMatch(source, /@callback=["']handleCallback["']/, "resource import should not also add images on checkbox callback");
+ assert.match(source, /normalizeSelectedImages\(this\.selectedImage\)/, "confirm should normalize selected resource objects");
+ assert.match(source, /this\.images\.push\(\.\.\.selectedImages\)/, "confirm should append normalized images once");
+});
diff --git a/seller/src/views/lili-components/editor/upload-image.vue b/seller/src/views/lili-components/editor/upload-image.vue
index 44ee38f7..dde6b3de 100644
--- a/seller/src/views/lili-components/editor/upload-image.vue
+++ b/seller/src/views/lili-components/editor/upload-image.vue
@@ -57,13 +57,14 @@
- { selectedImage = list}" @callback="handleCallback" />
+ { selectedImage = list}" />