diff --git a/README.md b/README.md index c04c646..a789f5d 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/config/api.js b/config/api.js index 171cefd..7b96fcc 100644 --- a/config/api.js +++ b/config/api.js @@ -7,15 +7,15 @@ const localImBase = process.env.VUE_APP_IM_API_BASE_URL || localApiBase; // 开发环境优先读取一键启动脚本生成的 .env.development.local。 const dev = { - im: localImBase || "https://im-api.pickmall.cn", - common: localApiBase || "https://common-api.pickmall.cn", - buyer: localApiBase || "https://buyer-api.pickmall.cn", + im: localImBase || "/api", + common: localApiBase || "/api", + buyer: localApiBase || "/api", }; -// 生产环境保持原有线上默认值,构建时也允许通过环境变量显式覆盖。 +// 生产环境默认走 All-In-One 同域入口,构建时仍允许通过环境变量显式覆盖。 const prod = { - im: localImBase || "https://im-api.pickmall.cn", - common: localApiBase || "https://common-api.pickmall.cn", - buyer: localApiBase || "https://buyer-api.pickmall.cn", + im: localImBase || "/api", + common: localApiBase || "/api", + buyer: localApiBase || "/api", }; //默认生产环境 diff --git a/config/config.js b/config/config.js index ce641b7..372383f 100644 --- a/config/config.js +++ b/config/config.js @@ -2,6 +2,19 @@ const name = "lilishop"; //全局商城name const schemeName = "lilishop"; //唤醒app需要的schemeName const wapUrl = process.env.VUE_APP_WAP_URL || "https://m-b2b2c.pickmall.cn"; const loginCaptchaBypass = process.env.VUE_APP_LOGIN_CAPTCHA_BYPASS === "true"; +const normalizeWsUrl = (url) => { + // H5 可使用同域相对路径,运行时按当前页面协议补齐 WebSocket 地址。 + if (!url || /^wss?:\/\//.test(url)) { + return url || ""; + } + // #ifdef H5 + if (typeof window !== "undefined" && url.startsWith("/")) { + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + return `${protocol}//${window.location.host}${url}`; + } + // #endif + return url; +}; export default { name: name, @@ -16,7 +29,7 @@ export default { customerServiceMobile: "13161366885", //客服电话 customerServiceEmail: "lili@lili.com", //客服邮箱 imWebSrc: process.env.VUE_APP_IM_WEB_URL || "https://im.pickmall.cn", //IM地址 - baseWsUrl: process.env.VUE_APP_IM_WS_URL || "wss://im-api.pickmall.cn/lili/webSocket", // IM WS 地址 + baseWsUrl: normalizeWsUrl(process.env.VUE_APP_IM_WS_URL || "/lili/webSocket"), // IM WS 地址 loginCaptchaBypass, enableGetClipboard: false, //是否启用粘贴板获取 scanAuthNavigation 中的链接,如果匹配则会跳转到对应页面 enableMiniBarStartUpApp: true, //是否在h5中右侧浮空按钮点击启动app diff --git a/pages/product/comment.vue b/pages/product/comment.vue index 0cc84e4..4d77e6e 100644 --- a/pages/product/comment.vue +++ b/pages/product/comment.vue @@ -321,7 +321,7 @@ page { .star { width: 30rpx; height: 30rpx; - background: url("/static/star.png"); + background: url("../../static/star.png"); background-size: 100%; } } diff --git a/pages/product/product/promotion/group.scss b/pages/product/product/promotion/group.scss index 0fec212..d4f1513 100644 --- a/pages/product/product/promotion/group.scss +++ b/pages/product/product/promotion/group.scss @@ -1,5 +1,5 @@ .group-wrapper { - background: url("/static/exchange.png"); + background: url("../../../../static/exchange.png"); background-size: cover; } .u-group-row { diff --git a/pages/promotion/joinGroup.vue b/pages/promotion/joinGroup.vue index 0053229..16fd4cd 100644 --- a/pages/promotion/joinGroup.vue +++ b/pages/promotion/joinGroup.vue @@ -119,7 +119,7 @@ \ No newline at end of file + diff --git a/pages/promotion/seckill.vue b/pages/promotion/seckill.vue index e466527..43cb0c5 100644 --- a/pages/promotion/seckill.vue +++ b/pages/promotion/seckill.vue @@ -168,7 +168,7 @@ } .header-wraper { - background: url('/static/bg.png'); + background: url('../../static/bg.png'); height: 200rpx; display: flex; align-items: center; diff --git a/pages/tabbar/user/my.contract.test.mjs b/pages/tabbar/user/my.contract.test.mjs new file mode 100644 index 0000000..3457e12 --- /dev/null +++ b/pages/tabbar/user/my.contract.test.mjs @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const source = readFileSync(new URL('./my.vue', import.meta.url), 'utf8'); + +test('member center header background resolves under the H5 base path', () => { + assert.doesNotMatch(source, /background-image:\s*url\(["']\/static\/img\/main-bg\.png["']\)/); + assert.match(source, /background-image:\s*url\(["']\.\.\/\.\.\/\.\.\/static\/img\/main-bg\.png["']\)/); +}); diff --git a/pages/tabbar/user/my.vue b/pages/tabbar/user/my.vue index 6db1db6..c239dc0 100644 --- a/pages/tabbar/user/my.vue +++ b/pages/tabbar/user/my.vue @@ -174,7 +174,7 @@ body { background-size: cover; border-bottom-left-radius: 30rpx; border-bottom-right-radius: 30rpx; - background-image: url("/static/img/main-bg.png"); + background-image: url("../../../static/img/main-bg.png"); background-position: bottom; background-repeat: no-repeat; color: #ffffff; diff --git a/static-asset-url.contract.test.mjs b/static-asset-url.contract.test.mjs new file mode 100644 index 0000000..cbc97bc --- /dev/null +++ b/static-asset-url.contract.test.mjs @@ -0,0 +1,143 @@ +import assert from 'node:assert/strict'; +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import valueParser from 'postcss-value-parser'; +import vueTemplateCompiler from 'vue-template-compiler'; + +const roots = ['pages', 'components']; +const extraFiles = ['App.vue', 'uni.scss']; +const styleExtensions = new Set(['.css', '.scss']); + +function listFiles(dir) { + const result = []; + for (const entry of readdirSync(dir)) { + const fullPath = path.join(dir, entry); + const stat = statSync(fullPath); + if (stat.isDirectory()) { + result.push(...listFiles(fullPath)); + } else { + result.push(fullPath); + } + } + return result; +} + +function stripCssCommentsPreserveOffsets(source) { + let output = ''; + let quote = null; + + for (let i = 0; i < source.length; i += 1) { + const char = source[i]; + const next = source[i + 1]; + + if (quote) { + output += char; + if (char === '\\') { + i += 1; + output += source[i] || ''; + } else if (char === quote) { + quote = null; + } + continue; + } + + if (char === '"' || char === "'") { + quote = char; + output += char; + continue; + } + + if (char === '/' && next === '*') { + output += ' '; + i += 2; + while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) { + output += source[i] === '\n' ? '\n' : ' '; + i += 1; + } + output += ' '; + i += 1; + continue; + } + + if (char === '/' && next === '/') { + output += ' '; + i += 2; + while (i < source.length && source[i] !== '\n') { + output += ' '; + i += 1; + } + if (source[i] === '\n') { + output += '\n'; + } + continue; + } + + output += char; + } + + return output; +} + +function lineForOffset(source, offset) { + return source.slice(0, offset).split('\n').length; +} + +function collectRootStaticStyleUrls(filePath, styleSource, sourceOffset = 0, originalSource = styleSource) { + const stripped = stripCssCommentsPreserveOffsets(styleSource); + const findings = []; + + valueParser(stripped).walk((node) => { + if (node.type !== 'function' || node.value.toLowerCase() !== 'url') { + return; + } + + const url = valueParser.stringify(node.nodes).trim().replace(/^['"]|['"]$/g, ''); + if (!url.startsWith('/static/')) { + return; + } + + findings.push({ + filePath, + line: lineForOffset(originalSource, sourceOffset + node.sourceIndex), + url, + }); + }); + + return findings; +} + +function styleSourcesForFile(filePath) { + const source = readFileSync(filePath, 'utf8'); + if (filePath.endsWith('.vue')) { + const descriptor = vueTemplateCompiler.parseComponent(source); + return descriptor.styles.map((style) => ({ + source: style.content, + sourceOffset: style.start, + originalSource: source, + })); + } + + return [{ source, sourceOffset: 0, originalSource: source }]; +} + +test('style url() references do not use H5-root /static paths', () => { + const files = [ + ...roots.flatMap((root) => listFiles(root)), + ...extraFiles, + ].filter((filePath) => filePath.endsWith('.vue') || styleExtensions.has(path.extname(filePath))); + + const findings = files.flatMap((filePath) => + styleSourcesForFile(filePath).flatMap(({ source, sourceOffset, originalSource }) => + collectRootStaticStyleUrls(filePath, source, sourceOffset, originalSource) + ) + ); + + assert.deepEqual( + findings, + [], + `style url() must use relative static paths so H5 /h5 deployments do not request site-root assets:\n${ + findings.map((item) => `${item.filePath}:${item.line} ${item.url}`).join('\n') + }` + ); +});