From ffeaa23f01e188c0021100f065f1b75d1383f4f5 Mon Sep 17 00:00:00 2001 From: wanghai Date: Fri, 12 Jun 2026 19:55:13 +0800 Subject: [PATCH] 4.6.4 --- .gitignore | 2 + composer.json | 2 +- config/common.php | 1 + dev-setup.sh | 464 ++++++++++-------- docker-compose.dev.yml | 13 + docker-dev/Dockerfile | 2 +- docker-dev/nginx.conf | 4 +- docker-dev/supervisord.conf | 13 - routes/api/member.php | 1 + routes/frontapi/goods.php | 2 + .../Http/FrontApi/V1/Action/Distributor.php | 6 +- .../Http/FrontApi/V1/Action/StoreHomePage.php | 2 +- .../Services/StoreHomePageService.php | 243 +++++++-- .../Http/FrontApi/V1/Action/Category.php | 57 ++- .../Http/FrontApi/V1/Action/Items.php | 50 ++ .../Services/ItemsCategoryService.php | 60 +++ .../Http/Api/V1/Action/DiscountCard.php | 14 +- .../Support/ShopDiscountCardListFilter.php | 77 +++ .../Http/Api/V1/Action/Members.php | 64 ++- .../Http/FrontApi/V1/Action/Members.php | 9 +- .../Services/MemberRegSettingService.php | 4 + src/OrdersBundle/Http/Api/V1/Action/Order.php | 2 +- .../Services/Cart/DistributorCartObject.php | 2 +- src/OrdersBundle/Services/CartService.php | 2 +- src/OrdersBundle/Services/OrderService.php | 2 +- src/OrdersBundle/Traits/GetOrderIdTrait.php | 98 +++- .../Traits/CheckPromotionsValid.php | 2 +- .../Http/FrontApi/V1/Action/OpenScreenAd.php | 19 +- .../Http/FrontApi/V1/Action/PagesTemplate.php | 13 +- .../Services/OpenScreenAdServices.php | 13 +- .../Services/PagesTemplateServices.php | 12 +- src/WechatBundle/Http/Api/V1/Action/Wxa.php | 3 +- tests/GoodsBatchItemsActionTest.php | 83 ++++ .../StoreHomePageServiceInternalNameTest.php | 102 ++++ 34 files changed, 1089 insertions(+), 354 deletions(-) create mode 100644 src/KaquanBundle/Support/ShopDiscountCardListFilter.php create mode 100644 tests/GoodsBatchItemsActionTest.php diff --git a/.gitignore b/.gitignore index e3a3226..6a4c4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ docker-dev/seed-*.sql docker-dev/patch-*.sql docker-dev/extract-weapp-staging.py docker-compose.override.yml + +.codegraph/ diff --git a/composer.json b/composer.json index cc96c29..8f23e18 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "laravel/lumen", - "version": "4.6.3", + "version": "4.6.4", "description": "The Laravel Lumen Framework.", "keywords": [ "framework", diff --git a/config/common.php b/config/common.php index 822bf9d..6ce4ffb 100644 --- a/config/common.php +++ b/config/common.php @@ -108,6 +108,7 @@ return [ 'encrypt_sensitive_data' => env('ENCRYPT_SENSITIVE_DATA', false), 'sms_send_limit' => env("SMS_SEND_LIMIT", 5), // 一种短信验证码在一天里的发送上限 + 'sms_debug_vcode' => env('SMS_DEBUG_VCODE', false), // 开发调试短信验证码:开启后不调用真实短信通道,直接返回验证码 'shop_admin_url' => env('SHOP_ADMIN_URL', ''),// 管理后台地址,结尾带斜杠 'appcenter_base_url' => env('APPCENTER_BASE_URL', env('TEST_MODE', false) ? 'https://account.uc.ex-sandbox.com' : 'https://account.shopex.cn'), diff --git a/dev-setup.sh b/dev-setup.sh index 6839c8f..d2cbd36 100755 --- a/dev-setup.sh +++ b/dev-setup.sh @@ -1,8 +1,8 @@ #!/bin/bash # ECShopX 开发环境设置脚本(Docker 方式) -# 使用单容器运行所有服务:PHP-FPM、Nginx、MySQL、Redis -# 支持三个项目:ECShopX、ECShopX_admin-frontend、ECShopX_mobile-frontend +# 主容器运行 PHP-FPM、Nginx、MySQL、Redis,Web 前端使用独立 Node 容器 +# 支持四个项目:ECShopX、ECShopX_admin-frontend、ECShopX_mobile-frontend、ECShopX_web-frontend set -e @@ -17,6 +17,7 @@ PARENT_DIR="$(cd "$PROJECT_ROOT/.." && pwd)" # 容器配置 CONTAINER_NAME="ecshopx-dev" +WEB_CONTAINER_NAME="ecshopx-web-frontend" DOCKER_COMPOSE_FILE="$PROJECT_ROOT/docker-compose.dev.yml" # 数据库配置 @@ -197,6 +198,18 @@ is_container_running() { docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$" 2>/dev/null } +is_web_container_running() { + docker ps --format '{{.Names}}' | grep -q "^${WEB_CONTAINER_NAME}$" 2>/dev/null +} + +ensure_web_container_running() { + if ! is_web_container_running; then + log_error "Web 前端容器 $WEB_CONTAINER_NAME 未运行,请先启动 Docker Compose" + return 1 + fi + return 0 +} + # =========================================== # 检查容器是否存在(包括已停止的) # =========================================== @@ -344,6 +357,62 @@ check_docker() { check_and_clone_frontend() { log_step "检查前端项目目录..." + # 检查 ECShopX_web-frontend + PC_DIR="$PARENT_DIR/ECShopX_web-frontend" + PC_REPO="https://gitee.com/ShopeX/ECShopX_web-frontend.git" + + if [ ! -d "$PC_DIR" ] || [ ! -f "$PC_DIR/package.json" ]; then + if [ ! -d "$PC_DIR" ]; then + log_warning "ECShopX_web-frontend 目录不存在" + else + log_warning "ECShopX_web-frontend 目录存在但缺少 package.json" + fi + + echo "" + echo -n "是否从 Gitee 克隆PC商城(ECShopX_web-frontend)代码? [Y/n]: " + read -r answer < /dev/tty + + if [ -z "$answer" ] || [ "$answer" = "Y" ] || [ "$answer" = "y" ] || [ "$answer" = "yes" ] || [ "$answer" = "YES" ]; then + if [ -d "$PC_DIR" ]; then + log_info "清空现有目录内容..." + find "$PC_DIR" -mindepth 1 -delete 2>/dev/null + fi + + log_info "正在从 Gitee 克隆PC商城(ECShopX_web-frontend)..." + git clone "$PC_REPO" "$PC_DIR" > /tmp/git_clone_pc.log 2>&1 & + local clone_pid=$! + + # 显示进度动画 + local spinstr='|/-\' + while kill -0 $clone_pid 2>/dev/null; do + local temp=${spinstr#?} + printf "\r${CYAN}[INFO]${NC} 克隆PC商城(ECShopX_web-frontend)中 ${spinstr:0:1}" + spinstr=$temp${spinstr%"$temp"} + sleep 0.2 + done + wait $clone_pid + local clone_exit=$? + + if [ $clone_exit -eq 0 ]; then + printf "\r${GREEN}[SUCCESS]${NC}PC商城(ECShopX_web-frontend)克隆成功" + printf "%50s" "" + echo "" + INSTALLED_PC=true + else + printf "\r${RED}[ERROR]${NC}PC商城(ECShopX_web-frontend)克隆失败" + printf "%50s" "" + echo "" + cat /tmp/git_clone_pc.log 2>/dev/null | tail -10 + exit 1 + fi + else + log_warning "跳过PC商城(ECShopX_web-frontend)克隆" + fi + else + log_info "ECShopX_web-frontend 目录已存在且包含 package.json,跳过克隆" + INSTALLED_PC=true + fi + # 检查 ECShopX_admin-frontend ADMIN_DIR="$PARENT_DIR/ECShopX_admin-frontend" ADMIN_REPO="https://gitee.com/ShopeX/ECShopX_admin-frontend.git" @@ -460,62 +529,6 @@ check_and_clone_frontend() { INSTALLED_VSHOP=true fi - # 检查 ECShopX_desktop-frontend - PC_DIR="$PARENT_DIR/ECShopX_desktop-frontend" - PC_REPO="https://gitee.com/ShopeX/ECShopX_desktop-frontend.git" - - if [ ! -d "$PC_DIR" ] || [ ! -f "$PC_DIR/package.json" ]; then - if [ ! -d "$PC_DIR" ]; then - log_warning "ECShopX_desktop-frontend 目录不存在" - else - log_warning "ECShopX_desktop-frontend 目录存在但缺少 package.json" - fi - - echo "" - echo -n "是否从 Gitee 克隆PC商城(ECShopX_desktop-frontend)代码? [Y/n]: " - read -r answer < /dev/tty - - if [ -z "$answer" ] || [ "$answer" = "Y" ] || [ "$answer" = "y" ] || [ "$answer" = "yes" ] || [ "$answer" = "YES" ]; then - if [ -d "$PC_DIR" ]; then - log_info "清空现有目录内容..." - find "$PC_DIR" -mindepth 1 -delete 2>/dev/null - fi - - log_info "正在从 Gitee 克隆PC商城(ECShopX_desktop-frontend)..." - git clone "$PC_REPO" "$PC_DIR" > /tmp/git_clone_pc.log 2>&1 & - local clone_pid=$! - - # 显示进度动画 - local spinstr='|/-\' - while kill -0 $clone_pid 2>/dev/null; do - local temp=${spinstr#?} - printf "\r${CYAN}[INFO]${NC} 克隆PC商城(ECShopX_desktop-frontend)中 ${spinstr:0:1}" - spinstr=$temp${spinstr%"$temp"} - sleep 0.2 - done - wait $clone_pid - local clone_exit=$? - - if [ $clone_exit -eq 0 ]; then - printf "\r${GREEN}[SUCCESS]${NC}PC商城(ECShopX_desktop-frontend)克隆成功" - printf "%50s" "" - echo "" - INSTALLED_PC=true - else - printf "\r${RED}[ERROR]${NC}PC商城(ECShopX_desktop-frontend)克隆失败" - printf "%50s" "" - echo "" - cat /tmp/git_clone_pc.log 2>/dev/null | tail -10 - exit 1 - fi - else - log_warning "跳过PC商城(ECShopX_desktop-frontend)克隆" - fi - else - log_info "ECShopX_desktop-frontend 目录已存在且包含 package.json,跳过克隆" - INSTALLED_PC=true - fi - echo "" } @@ -544,12 +557,16 @@ check_container_status() { ;; 2) log_info "使用现有容器..." + log_info "确保 Docker Compose 中的所有服务已启动..." + $DOCKER_COMPOSE_CMD -f "$DOCKER_COMPOSE_FILE" up -d # 如果容器未运行,先启动它 if ! is_container_running; then log_info "启动现有容器..." - $DOCKER_COMPOSE_CMD -f "$DOCKER_COMPOSE_FILE" up -d sleep 5 wait_for_services + elif ! is_web_container_running; then + log_info "等待 Web 前端容器启动..." + sleep 5 fi return 1 # 跳过构建 ;; @@ -559,6 +576,8 @@ check_container_status() { ;; *) log_info "使用现有容器..." + log_info "确保 Docker Compose 中的所有服务已启动..." + $DOCKER_COMPOSE_CMD -f "$DOCKER_COMPOSE_FILE" up -d return 1 ;; esac @@ -1048,54 +1067,50 @@ check_container_directory() { local project_name=$3 local max_retries=2 local retry_count=0 + local target_container="$CONTAINER_NAME" + + if [ "$project_name" = "ECShopX_web-frontend" ]; then + target_container="$WEB_CONTAINER_NAME" + fi # 首先检查容器是否运行 - if ! is_container_running; then - log_warning "容器未运行,无法检查目录挂载状态" + if ! docker ps --format '{{.Names}}' | grep -q "^${target_container}$" 2>/dev/null; then + log_warning "容器 $target_container 未运行,无法检查目录挂载状态" log_info "目录挂载将在容器启动后自动生效" - # 对于 ECShopX_desktop-frontend,如果容器未运行,也返回成功(允许继续) - if [ "$project_name" = "ECShopX_desktop-frontend" ]; then - return 0 - fi return 0 # 容器未运行时,假设挂载会在启动后生效 fi while [ $retry_count -lt $max_retries ]; do # 检查目录是否存在 - if docker exec "$CONTAINER_NAME" sh -c "test -d $container_path" 2>/dev/null; then + if docker exec "$target_container" sh -c "test -d $container_path" 2>/dev/null; then # 检查 package.json 是否存在 - if docker exec "$CONTAINER_NAME" sh -c "test -f $container_path/package.json" 2>/dev/null; then - log_success "目录挂载检查通过: $container_path" + if docker exec "$target_container" sh -c "test -f $container_path/package.json" 2>/dev/null; then + log_success "目录挂载检查通过: $target_container:$container_path" return 0 # 目录和文件都存在 else if [ $retry_count -eq 0 ]; then - log_warning "容器内 $container_path/package.json 不存在,尝试重启容器以确保目录正确挂载..." + log_warning "容器 $target_container 内 $container_path/package.json 不存在,尝试重启容器以确保目录正确挂载..." if ! restart_container_for_mount "$project_name"; then return 1 fi retry_count=$((retry_count + 1)) continue else - # 对于 ECShopX_desktop-frontend,如果重启后仍然不存在,也允许继续(可能是配置问题) - if [ "$project_name" = "ECShopX_desktop-frontend" ]; then - log_warning "容器内 $container_path/package.json 仍然不存在,但继续执行..." - return 0 - fi - log_error "容器内 $container_path/package.json 仍然不存在" + log_error "容器 $target_container 内 $container_path/package.json 仍然不存在" log_info "请检查主机目录 $host_path 是否存在且包含 package.json" return 1 fi fi else if [ $retry_count -eq 0 ]; then - log_warning "容器内 $container_path 目录不存在,尝试重启容器以确保目录正确挂载..." + log_warning "容器 $target_container 内 $container_path 目录不存在,尝试重启容器以确保目录正确挂载..." if ! restart_container_for_mount "$project_name"; then return 1 fi retry_count=$((retry_count + 1)) continue else - log_error "容器内 $container_path 目录仍然不存在" + log_error "容器 $target_container 内 $container_path 目录仍然不存在" log_info "请检查 docker-compose.dev.yml 中的卷挂载配置是否正确" log_info "主机目录路径: $host_path" return 1 @@ -1112,11 +1127,17 @@ check_container_directory() { restart_container_for_mount() { local project_name=$1 + local target_container="$CONTAINER_NAME" + + if [ "$project_name" = "ECShopX_web-frontend" ]; then + target_container="$WEB_CONTAINER_NAME" + fi + log_warning "检测到 $project_name 目录未正确挂载,正在重启容器..." - if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$" 2>/dev/null; then - log_info "重启容器 $CONTAINER_NAME..." - docker-compose -f "$DOCKER_COMPOSE_FILE" restart || { + if docker ps --format '{{.Names}}' | grep -q "^${target_container}$" 2>/dev/null; then + log_info "重启容器 $target_container..." + $DOCKER_COMPOSE_CMD -f "$DOCKER_COMPOSE_FILE" restart "$target_container" || { log_error "容器重启失败" return 1 } @@ -1124,8 +1145,10 @@ restart_container_for_mount() { log_info "等待容器启动..." sleep 5 - # 等待服务就绪 - wait_for_services + if [ "$target_container" = "$CONTAINER_NAME" ]; then + # 等待主服务就绪 + wait_for_services + fi log_success "容器重启完成" return 0 @@ -1145,6 +1168,7 @@ configure_frontend_env() { local api_base_url=${3:-"http://localhost:8080/api/"} local app_id=${4:-""} local default_lang=${5:-""} + local target_container="$CONTAINER_NAME" # 检查主机目录是否存在 if [ ! -d "$project_dir" ]; then @@ -1158,16 +1182,17 @@ configure_frontend_env() { container_path="/data/httpd/ECShopX_admin-frontend" elif [ "$project_name" = "ECShopX_mobile-frontend" ]; then container_path="/data/httpd/ECShopX_mobile-frontend" - elif [ "$project_name" = "ECShopX_desktop-frontend" ]; then - container_path="/data/httpd/ECShopX_desktop-frontend" + elif [ "$project_name" = "ECShopX_web-frontend" ]; then + container_path="/data/httpd/ECShopX_web-frontend" + target_container="$WEB_CONTAINER_NAME" else log_warning "未知的项目名称: $project_name" return 0 fi # 如果容器未运行,直接配置主机目录 - if ! is_container_running; then - log_info "容器未运行,配置主机目录的 .env 文件..." + if ! docker ps --format '{{.Names}}' | grep -q "^${target_container}$" 2>/dev/null; then + log_info "容器 $target_container 未运行,配置主机目录的 .env 文件..." local env_file="$project_dir/.env" # 如果 .env 不存在,尝试从 .env.example 复制 @@ -1223,6 +1248,21 @@ configure_frontend_env() { echo "VUE_APP_QIANKUN_ENTRY=http://localhost:8080/newpc/" >> "$env_file" fi log_success "已配置 VUE_APP_QIANKUN_ENTRY=http://localhost:8080/newpc/" + + # 配置 VUE_APP_WEBSITE(PC前端访问地址) + if [ "$INSTALLED_PC" = true ]; then + if grep -q "^VUE_APP_WEBSITE=" "$env_file" 2>/dev/null; then + # macOS 和 Linux 兼容的 sed 命令 + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^VUE_APP_WEBSITE=.*|VUE_APP_WEBSITE=http://localhost:8082|" "$env_file" + else + sed -i "s|^VUE_APP_WEBSITE=.*|VUE_APP_WEBSITE=http://localhost:8082|" "$env_file" + fi + else + echo "VUE_APP_WEBSITE=http://localhost:8082" >> "$env_file" + fi + log_success "已配置 VUE_APP_WEBSITE=http://localhost:8082" + fi fi # 配置 ECShopX_mobile-frontend @@ -1271,59 +1311,59 @@ configure_frontend_env() { fi fi - # 配置 ECShopX_desktop-frontend - if [ "$project_name" = "ECShopX_desktop-frontend" ]; then - # 使用 sed 修改或添加 VUE_APP_API_BASE_URL - if grep -q "^VUE_APP_API_BASE_URL=" "$env_file" 2>/dev/null; then + # 配置 ECShopX_web-frontend + if [ "$project_name" = "ECShopX_web-frontend" ]; then + # 使用 sed 修改或添加 NUXT_PUBLIC_API_BASE + if grep -q "^NUXT_PUBLIC_API_BASE=" "$env_file" 2>/dev/null; then # macOS 和 Linux 兼容的 sed 命令 if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s|^VUE_APP_API_BASE_URL=.*|VUE_APP_API_BASE_URL=$api_base_url|" "$env_file" + sed -i '' "s|^NUXT_PUBLIC_API_BASE=.*|NUXT_PUBLIC_API_BASE=$api_base_url/api/h5app|" "$env_file" else - sed -i "s|^VUE_APP_API_BASE_URL=.*|VUE_APP_API_BASE_URL=$api_base_url|" "$env_file" + sed -i "s|^NUXT_PUBLIC_API_BASE=.*|NUXT_PUBLIC_API_BASE=$api_base_url/api/h5app|" "$env_file" fi else - echo "VUE_APP_API_BASE_URL=$api_base_url" >> "$env_file" + echo "NUXT_PUBLIC_API_BASE=$api_base_url/api/h5app" >> "$env_file" fi - log_success "已配置 VUE_APP_API_BASE_URL=$api_base_url" + log_success "已配置 NUXT_PUBLIC_API_BASE=$api_base_url/api/h5app" - # 配置 VUE_APP_COMPANYID(默认值为1) - if grep -q "^VUE_APP_COMPANYID=" "$env_file" 2>/dev/null; then + # 配置 NUXT_PUBLIC_COMPANY_ID(默认值为1) + if grep -q "^NUXT_PUBLIC_COMPANY_ID=" "$env_file" 2>/dev/null; then # macOS 和 Linux 兼容的 sed 命令 if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s|^VUE_APP_COMPANYID=.*|VUE_APP_COMPANYID=1|" "$env_file" + sed -i '' "s|^NUXT_PUBLIC_COMPANY_ID=.*|NUXT_PUBLIC_COMPANY_ID=1|" "$env_file" else - sed -i "s|^VUE_APP_COMPANYID=.*|VUE_APP_COMPANYID=1|" "$env_file" + sed -i "s|^NUXT_PUBLIC_COMPANY_ID=.*|NUXT_PUBLIC_COMPANY_ID=1|" "$env_file" fi else - echo "VUE_APP_COMPANYID=1" >> "$env_file" + echo "NUXT_PUBLIC_COMPANY_ID=1" >> "$env_file" fi - log_success "已配置 VUE_APP_COMPANYID=1" + log_success "已配置 NUXT_PUBLIC_COMPANY_ID=1" - # 配置 VUE_APP_DEFAULT_LANG(如果提供) + # 配置 NUXT_PUBLIC_DEFAULT_COUNTRY_CODE(如果提供) if [ -n "$default_lang" ]; then - if grep -q "^VUE_APP_DEFAULT_LANG=" "$env_file" 2>/dev/null; then + if grep -q "^NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=" "$env_file" 2>/dev/null; then # macOS 和 Linux 兼容的 sed 命令 if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s|^VUE_APP_DEFAULT_LANG=.*|VUE_APP_DEFAULT_LANG=$default_lang|" "$env_file" + sed -i '' "s|^NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=.*|NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=$default_lang|" "$env_file" else - sed -i "s|^VUE_APP_DEFAULT_LANG=.*|VUE_APP_DEFAULT_LANG=$default_lang|" "$env_file" + sed -i "s|^NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=.*|NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=$default_lang|" "$env_file" fi else - echo "VUE_APP_DEFAULT_LANG=$default_lang" >> "$env_file" + echo "NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=$default_lang" >> "$env_file" fi - log_success "已配置 VUE_APP_DEFAULT_LANG=$default_lang" + log_success "已配置 NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=$default_lang" fi fi else # 容器运行中,配置容器内的 .env 文件 # 检查容器内目录是否存在 - if ! docker exec "$CONTAINER_NAME" sh -c "test -d $container_path" 2>/dev/null; then - log_warning "容器内 $container_path 目录不存在,跳过配置" + if ! docker exec "$target_container" sh -c "test -d $container_path" 2>/dev/null; then + log_warning "容器 $target_container 内 $container_path 目录不存在,跳过配置" return 0 fi # 在容器内配置 .env 文件 - docker exec "$CONTAINER_NAME" sh -c " + docker exec "$target_container" sh -c " cd $container_path && \ if [ ! -f .env ]; then if [ -f .env.example ]; then @@ -1336,7 +1376,7 @@ configure_frontend_env() { # 配置 ECShopX_admin-frontend if [ "$project_name" = "ECShopX_admin-frontend" ]; then - docker exec "$CONTAINER_NAME" sh -c " + docker exec "$target_container" sh -c " cd $container_path && \ if grep -q '^VUE_APP_BASE_API=' .env 2>/dev/null; then sed -i 's|^VUE_APP_BASE_API=.*|VUE_APP_BASE_API=$api_base_url|' .env @@ -1348,7 +1388,7 @@ configure_frontend_env() { # 配置 VUE_APP_DEFAULT_LANG(如果提供) if [ -n "$default_lang" ]; then - docker exec "$CONTAINER_NAME" sh -c " + docker exec "$target_container" sh -c " cd $container_path && \ if grep -q '^VUE_APP_DEFAULT_LANG=' .env 2>/dev/null; then sed -i 's|^VUE_APP_DEFAULT_LANG=.*|VUE_APP_DEFAULT_LANG=$default_lang|' .env @@ -1360,7 +1400,7 @@ configure_frontend_env() { fi # 配置 VUE_APP_QIANKUN_ENTRY - docker exec "$CONTAINER_NAME" sh -c " + docker exec "$target_container" sh -c " cd $container_path && \ if grep -q '^VUE_APP_QIANKUN_ENTRY=' .env 2>/dev/null; then sed -i 's|^VUE_APP_QIANKUN_ENTRY=.*|VUE_APP_QIANKUN_ENTRY=http://localhost:8080/newpc/|' .env @@ -1369,11 +1409,24 @@ configure_frontend_env() { fi " 2>/dev/null || true log_success "已配置容器内 VUE_APP_QIANKUN_ENTRY=http://localhost:8080/newpc/" + + # 配置 VUE_APP_WEBSITE(PC前端访问地址) + if [ "$INSTALLED_PC" = true ]; then + docker exec "$target_container" sh -c " + cd $container_path && \ + if grep -q '^VUE_APP_WEBSITE=' .env 2>/dev/null; then + sed -i 's|^VUE_APP_WEBSITE=.*|VUE_APP_WEBSITE=http://localhost:8082|' .env + else + echo 'VUE_APP_WEBSITE=http://localhost:8082' >> .env + fi + " 2>/dev/null || true + log_success "已配置容器内 VUE_APP_WEBSITE=http://localhost:8082" + fi fi # 配置 ECShopX_mobile-frontend if [ "$project_name" = "ECShopX_mobile-frontend" ]; then - docker exec "$CONTAINER_NAME" sh -c " + docker exec "$target_container" sh -c " cd $container_path && \ if grep -q '^APP_BASE_URL=' .env 2>/dev/null; then sed -i 's|^APP_BASE_URL=.*|APP_BASE_URL=$api_base_url|' .env @@ -1385,7 +1438,7 @@ configure_frontend_env() { # 配置 APP_PLATFORM(如果提供) if [ -n "$app_id" ]; then - docker exec "$CONTAINER_NAME" sh -c " + docker exec "$target_container" sh -c " cd $container_path && \ if grep -q '^APP_PLATFORM=' .env 2>/dev/null; then sed -i 's|^APP_PLATFORM=.*|APP_PLATFORM=$app_id|' .env @@ -1398,7 +1451,7 @@ configure_frontend_env() { # 配置 APP_I18N_ORIGIN_LANG(如果提供) if [ -n "$default_lang" ]; then - docker exec "$CONTAINER_NAME" sh -c " + docker exec "$target_container" sh -c " cd $container_path && \ if grep -q '^APP_I18N_ORIGIN_LANG=' .env 2>/dev/null; then sed -i 's|^APP_I18N_ORIGIN_LANG=.*|APP_I18N_ORIGIN_LANG=$default_lang|' .env @@ -1410,40 +1463,40 @@ configure_frontend_env() { fi fi - # 配置 ECShopX_desktop-frontend - if [ "$project_name" = "ECShopX_desktop-frontend" ]; then - docker exec "$CONTAINER_NAME" sh -c " + # 配置 ECShopX_web-frontend + if [ "$project_name" = "ECShopX_web-frontend" ]; then + docker exec "$target_container" sh -c " cd $container_path && \ - if grep -q '^VUE_APP_API_BASE_URL=' .env 2>/dev/null; then - sed -i 's|^VUE_APP_API_BASE_URL=.*|VUE_APP_API_BASE_URL=$api_base_url|' .env + if grep -q '^NUXT_PUBLIC_API_BASE=' .env 2>/dev/null; then + sed -i 's|^NUXT_PUBLIC_API_BASE=.*|NUXT_PUBLIC_API_BASE=$api_base_url/api/h5app|' .env else - echo 'VUE_APP_API_BASE_URL=$api_base_url' >> .env + echo 'NUXT_PUBLIC_API_BASE=$api_base_url/api/h5app' >> .env fi " 2>/dev/null || true - log_success "已配置容器内 VUE_APP_API_BASE_URL=$api_base_url" + log_success "已配置容器内 NUXT_PUBLIC_API_BASE=$api_base_url/api/h5app" - # 配置 VUE_APP_COMPANYID(默认值为1) - docker exec "$CONTAINER_NAME" sh -c " + # 配置 NUXT_PUBLIC_COMPANY_ID(默认值为1) + docker exec "$target_container" sh -c " cd $container_path && \ - if grep -q '^VUE_APP_COMPANYID=' .env 2>/dev/null; then - sed -i 's|^VUE_APP_COMPANYID=.*|VUE_APP_COMPANYID=1|' .env + if grep -q '^NUXT_PUBLIC_COMPANY_ID=' .env 2>/dev/null; then + sed -i 's|^NUXT_PUBLIC_COMPANY_ID=.*|NUXT_PUBLIC_COMPANY_ID=1|' .env else - echo 'VUE_APP_COMPANYID=1' >> .env + echo 'NUXT_PUBLIC_COMPANY_ID=1' >> .env fi " 2>/dev/null || true - log_success "已配置容器内 VUE_APP_COMPANYID=1" + log_success "已配置容器内 NUXT_PUBLIC_COMPANY_ID=1" - # 配置 VUE_APP_DEFAULT_LANG(如果提供) + # 配置 NUXT_PUBLIC_DEFAULT_COUNTRY_CODE(如果提供) if [ -n "$default_lang" ]; then - docker exec "$CONTAINER_NAME" sh -c " + docker exec "$target_container" sh -c " cd $container_path && \ - if grep -q '^VUE_APP_DEFAULT_LANG=' .env 2>/dev/null; then - sed -i 's|^VUE_APP_DEFAULT_LANG=.*|VUE_APP_DEFAULT_LANG=$default_lang|' .env + if grep -q '^NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=' .env 2>/dev/null; then + sed -i 's|^NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=.*|NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=$default_lang|' .env else - echo 'VUE_APP_DEFAULT_LANG=$default_lang' >> .env + echo 'NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=$default_lang' >> .env fi " 2>/dev/null || true - log_success "已配置容器内 VUE_APP_DEFAULT_LANG=$default_lang" + log_success "已配置容器内 NUXT_PUBLIC_DEFAULT_COUNTRY_CODE=$default_lang" fi fi fi @@ -1610,37 +1663,40 @@ build_admin() { } # =========================================== -# 编译 ECShopX_desktop-frontend +# 编译 ECShopX_web-frontend # =========================================== build_pc() { if [ "$SKIP_PC" = true ]; then - log_info "跳过 ECShopX_desktop-frontend 编译(--skip-pc)" + log_info "跳过 ECShopX_web-frontend 编译(--skip-pc)" return 0 fi - PC_DIR="$PARENT_DIR/ECShopX_desktop-frontend" + PC_DIR="$PARENT_DIR/ECShopX_web-frontend" if [ ! -d "$PC_DIR" ]; then - log_warning "ECShopX_desktop-frontend 目录不存在,跳过编译" + log_warning "ECShopX_web-frontend 目录不存在,跳过编译" return 0 fi log_info "==========================================" - log_info "开始编译 ECShopX_desktop-frontend(PC前端)..." + log_info "开始编译 ECShopX_web-frontend(PC前端)..." log_info "==========================================" if [ ! -f "$PC_DIR/package.json" ]; then - log_error "ECShopX_desktop-frontend/package.json 不存在" + log_error "ECShopX_web-frontend/package.json 不存在" return 1 fi - # 检查是否已有编译产物(Nuxt项目编译后生成.nuxt或.output目录) - if docker exec "$CONTAINER_NAME" sh -c "[ -d /data/httpd/ECShopX_desktop-frontend/.nuxt ]" 2>/dev/null || \ - docker exec "$CONTAINER_NAME" sh -c "[ -d /data/httpd/ECShopX_desktop-frontend/.output ]" 2>/dev/null || \ - [ -d "$PC_DIR/.nuxt" ] || [ -d "$PC_DIR/.output" ]; then - log_info "检测到已有编译产物,跳过编译" - log_info "如需重新编译,请删除 ECShopX_desktop-frontend/.nuxt 或 .output 目录" + if ! ensure_web_container_running; then + return 1 + fi + + # 检查是否已有可启动的 Nuxt 生产产物(.nuxt 可能由 pnpm install/postinstall 生成,不能代表已编译完成) + if docker exec "$WEB_CONTAINER_NAME" sh -c "[ -f /data/httpd/ECShopX_web-frontend/.output/server/index.mjs ]" 2>/dev/null || \ + [ -f "$PC_DIR/.output/server/index.mjs" ]; then + log_info "检测到已有 Nuxt 生产编译产物,跳过编译" + log_info "如需重新编译,请删除 ECShopX_web-frontend/.output 目录" # 即使跳过编译,也需要启动Nuxt服务 need_build=false else @@ -1649,7 +1705,7 @@ build_pc() { # 检查容器内目录是否存在并确保正确挂载 log_info "检查容器内目录挂载状态..." - if ! check_container_directory "/data/httpd/ECShopX_desktop-frontend" "$PC_DIR" "ECShopX_desktop-frontend"; then + if ! check_container_directory "/data/httpd/ECShopX_web-frontend" "$PC_DIR" "ECShopX_web-frontend"; then return 1 fi @@ -1659,21 +1715,14 @@ build_pc() { log_info "启动 Nuxt 服务(监听3000端口)..." # 检查是否已有Nuxt进程在运行 - if docker exec "$CONTAINER_NAME" sh -c "pgrep -f 'nuxt.*3000\|node.*nuxt' > /dev/null" 2>/dev/null; then + if docker exec "$WEB_CONTAINER_NAME" sh -c "pgrep -f 'nuxt.*3000|pnpm.*preview|pnpm.*dev|node .*\\.output/server/index\\.mjs' > /dev/null" 2>/dev/null; then log_info "Nuxt 服务已在运行" else - # 启动Nuxt服务 - if docker exec "$CONTAINER_NAME" sh -c "cd /data/httpd/ECShopX_desktop-frontend && npm run | grep -q 'start'" 2>/dev/null; then - docker exec -d "$CONTAINER_NAME" sh -c " - cd /data/httpd/ECShopX_desktop-frontend && \ - PORT=3000 HOST=0.0.0.0 nohup npm run start > /var/log/nuxt.log 2>&1 & - " 2>/dev/null || true - else - docker exec -d "$CONTAINER_NAME" sh -c " - cd /data/httpd/ECShopX_desktop-frontend && \ - PORT=3000 HOST=0.0.0.0 nohup npm run dev > /var/log/nuxt.log 2>&1 & - " 2>/dev/null || true - fi + # 启动已编译的 Nuxt 服务 + docker exec -d "$WEB_CONTAINER_NAME" sh -c " + cd /data/httpd/ECShopX_web-frontend && \ + NITRO_HOST=0.0.0.0 NITRO_PORT=3000 HOST=0.0.0.0 PORT=3000 nohup node .output/server/index.mjs > /var/log/nuxt.log 2>&1 & + " 2>/dev/null || true # 等待Nuxt服务启动 sleep 3 @@ -1714,18 +1763,19 @@ build_pc() { fi # 配置前端项目的 .env 文件 - log_info "配置 ECShopX_desktop-frontend 的 .env 文件..." - configure_frontend_env "$PC_DIR" "ECShopX_desktop-frontend" "http://localhost:8080" "" "$SELECTED_LANG" + log_info "配置 ECShopX_web-frontend 的 .env 文件..." + configure_frontend_env "$PC_DIR" "ECShopX_web-frontend" "http://localhost:8080" "" "$SELECTED_LANG" - log_info "安装 npm 依赖..." + log_info "启用 pnpm 并安装依赖..." # 使用绝对路径并先验证目录存在 - docker exec "$CONTAINER_NAME" sh -c " - if [ ! -d /data/httpd/ECShopX_desktop-frontend ]; then + docker exec "$WEB_CONTAINER_NAME" sh -c " + if [ ! -d /data/httpd/ECShopX_web-frontend ]; then echo '错误: 目录不存在' exit 1 fi - cd /data/httpd/ECShopX_desktop-frontend || exit 1 - npm install --legacy-peer-deps + cd /data/httpd/ECShopX_web-frontend || exit 1 + corepack enable + PNPM_HOME=/tmp/pnpm pnpm install --store-dir /tmp/pnpm-store " > /tmp/npm_pc_output.log 2>&1 & local npm_pid=$! @@ -1733,7 +1783,7 @@ build_pc() { local spinstr='|/-\' while kill -0 $npm_pid 2>/dev/null; do local temp=${spinstr#?} - printf "\r${CYAN}[INFO]${NC} 安装 ECShopX_desktop-frontend npm 依赖中 ${spinstr:0:1}" + printf "\r${CYAN}[INFO]${NC} 安装 ECShopX_web-frontend pnpm 依赖中 ${spinstr:0:1}" spinstr=$temp${spinstr%"$temp"} sleep 0.2 done @@ -1741,27 +1791,27 @@ build_pc() { local npm_exit=$? if [ $npm_exit -ne 0 ]; then - printf "\r${RED}[ERROR]${NC} ECShopX_desktop-frontend npm install 失败" + printf "\r${RED}[ERROR]${NC} ECShopX_web-frontend pnpm install 失败" printf "%50s" "" echo "" cat /tmp/npm_pc_output.log 2>/dev/null | tail -20 - log_info "请检查容器内目录状态: docker exec $CONTAINER_NAME ls -la /data/httpd/ECShopX_desktop-frontend" + log_info "请检查容器内目录状态: docker exec $WEB_CONTAINER_NAME ls -la /data/httpd/ECShopX_web-frontend" return 1 else - printf "\r${GREEN}[SUCCESS]${NC} ECShopX_desktop-frontend npm 依赖安装完成" + printf "\r${GREEN}[SUCCESS]${NC} ECShopX_web-frontend pnpm 依赖安装完成" printf "%50s" "" echo "" fi - log_info "执行编译(npm run build)..." - docker exec "$CONTAINER_NAME" sh -c "cd /data/httpd/ECShopX_desktop-frontend && npm run build" > /tmp/build_pc_output.log 2>&1 & + log_info "执行编译(pnpm build)..." + docker exec "$WEB_CONTAINER_NAME" sh -c "cd /data/httpd/ECShopX_web-frontend && PNPM_HOME=/tmp/pnpm pnpm build" > /tmp/build_pc_output.log 2>&1 & local build_pid=$! # 显示进度动画 local spinstr='|/-\' while kill -0 $build_pid 2>/dev/null; do local temp=${spinstr#?} - printf "\r${CYAN}[INFO]${NC} 编译 ECShopX_desktop-frontend 中 ${spinstr:0:1}" + printf "\r${CYAN}[INFO]${NC} 编译 ECShopX_web-frontend 中 ${spinstr:0:1}" spinstr=$temp${spinstr%"$temp"} sleep 0.2 done @@ -1769,67 +1819,53 @@ build_pc() { local build_exit=$? if [ $build_exit -ne 0 ]; then - printf "\r${RED}[ERROR]${NC} ECShopX_desktop-frontend 编译失败" + printf "\r${RED}[ERROR]${NC} ECShopX_web-frontend 编译失败" printf "%50s" "" echo "" cat /tmp/build_pc_output.log 2>/dev/null | tail -20 return 1 else - printf "\r${GREEN}[SUCCESS]${NC} ECShopX_desktop-frontend 编译完成" + printf "\r${GREEN}[SUCCESS]${NC} ECShopX_web-frontend 编译完成" printf "%50s" "" echo "" fi # 验证编译产物 - if ! docker exec "$CONTAINER_NAME" sh -c "[ -d /data/httpd/ECShopX_desktop-frontend/.nuxt ] || [ -d /data/httpd/ECShopX_desktop-frontend/.output ]" 2>/dev/null; then - log_error "ECShopX_desktop-frontend 编译输出不完整" + if ! docker exec "$WEB_CONTAINER_NAME" sh -c "[ -f /data/httpd/ECShopX_web-frontend/.output/server/index.mjs ]" 2>/dev/null; then + log_error "ECShopX_web-frontend 编译输出不完整" return 1 fi fi - # 检查编译产物(Nuxt项目编译后生成.nuxt或.output目录) - if docker exec "$CONTAINER_NAME" sh -c "[ -d /data/httpd/ECShopX_desktop-frontend/.nuxt ] || [ -d /data/httpd/ECShopX_desktop-frontend/.output ]" 2>/dev/null; then - log_success "ECShopX_desktop-frontend 编译成功" + # 检查可启动的 Nuxt 生产编译产物 + if docker exec "$WEB_CONTAINER_NAME" sh -c "[ -f /data/httpd/ECShopX_web-frontend/.output/server/index.mjs ]" 2>/dev/null; then + log_success "ECShopX_web-frontend 编译成功" # 启动 Nuxt 服务 log_info "启动 Nuxt 服务(监听3000端口)..." # 检查是否已有Nuxt进程在运行 - if docker exec "$CONTAINER_NAME" sh -c "pgrep -f 'nuxt.*3000\|node.*nuxt' > /dev/null" 2>/dev/null; then + if docker exec "$WEB_CONTAINER_NAME" sh -c "pgrep -f 'nuxt.*3000|pnpm.*preview|pnpm.*dev|node .*\\.output/server/index\\.mjs' > /dev/null" 2>/dev/null; then log_info "Nuxt 服务已在运行,重启服务..." - docker exec "$CONTAINER_NAME" sh -c "pkill -f 'nuxt.*3000\|node.*nuxt'" 2>/dev/null || true + docker exec "$WEB_CONTAINER_NAME" sh -c "pkill -f 'nuxt.*3000|pnpm.*preview|pnpm.*dev|node .*\\.output/server/index\\.mjs'" 2>/dev/null || true sleep 2 fi # 在后台启动Nuxt服务 - # 优先使用生产模式(npm run start),如果不存在则使用开发模式(npm run dev) - log_info "检查可用的启动命令..." - if docker exec "$CONTAINER_NAME" sh -c "cd /data/httpd/ECShopX_desktop-frontend && npm run | grep -q 'start'" 2>/dev/null; then - log_info "使用生产模式启动(npm run start)..." - docker exec -d "$CONTAINER_NAME" sh -c " - cd /data/httpd/ECShopX_desktop-frontend && \ - PORT=3000 HOST=0.0.0.0 nohup npm run start > /var/log/nuxt.log 2>&1 & - " || { - log_error "Nuxt 服务启动失败" - log_info "请检查日志: docker exec $CONTAINER_NAME tail -f /var/log/nuxt.log" - return 1 - } - else - log_info "使用开发模式启动(npm run dev)..." - docker exec -d "$CONTAINER_NAME" sh -c " - cd /data/httpd/ECShopX_desktop-frontend && \ - PORT=3000 HOST=0.0.0.0 nohup npm run dev > /var/log/nuxt.log 2>&1 & - " || { - log_error "Nuxt 服务启动失败" - log_info "请检查日志: docker exec $CONTAINER_NAME tail -f /var/log/nuxt.log" - return 1 - } - fi + log_info "使用生产模式启动(node .output/server/index.mjs)..." + docker exec -d "$WEB_CONTAINER_NAME" sh -c " + cd /data/httpd/ECShopX_web-frontend && \ + NITRO_HOST=0.0.0.0 NITRO_PORT=3000 HOST=0.0.0.0 PORT=3000 nohup node .output/server/index.mjs > /var/log/nuxt.log 2>&1 & + " || { + log_error "Nuxt 服务启动失败" + log_info "请检查日志: docker exec $WEB_CONTAINER_NAME tail -f /var/log/nuxt.log" + return 1 + } # 等待Nuxt服务启动 log_info "等待 Nuxt 服务启动..." local nuxt_ready=false for i in {1..30}; do - if docker exec "$CONTAINER_NAME" sh -c "curl -s http://127.0.0.1:3000 > /dev/null" 2>/dev/null; then + if docker exec "$WEB_CONTAINER_NAME" sh -c "wget -q --spider http://127.0.0.1:3000 >/dev/null 2>&1 || wget -S -O /dev/null http://127.0.0.1:3000 2>&1 | grep -q 'HTTP/'" 2>/dev/null; then nuxt_ready=true break fi @@ -1853,7 +1889,7 @@ build_pc() { printf "\r${YELLOW}[WARNING]${NC} Nuxt 服务启动超时,但可能仍在启动中" printf "%50s" "" echo "" - log_info "请检查日志: docker exec $CONTAINER_NAME tail -f /var/log/nuxt.log" + log_info "请检查日志: docker exec $WEB_CONTAINER_NAME tail -f /var/log/nuxt.log" if [ "$INSTALLED_VSHOP" = true ]; then log_info "H5前端访问地址: http://localhost:8081" fi @@ -1868,7 +1904,7 @@ build_pc() { log_warning "Nginx 配置重载失败,可能需要重启容器" } else - log_error "ECShopX_desktop-frontend 编译输出不完整" + log_error "ECShopX_web-frontend 编译输出不完整" return 1 fi } @@ -2304,8 +2340,10 @@ show_success_info() { fi log_info "常用命令:" log_info " 查看日志: $DOCKER_COMPOSE_CMD -f $DOCKER_COMPOSE_FILE logs -f" - log_info " 服务状态: docker exec $CONTAINER_NAME supervisorctl status" - log_info " 进入容器: docker exec -it $CONTAINER_NAME sh" + log_info " 主服务状态: docker exec $CONTAINER_NAME supervisorctl status" + log_info " 进入主容器: docker exec -it $CONTAINER_NAME sh" + log_info " 进入Web容器: docker exec -it $WEB_CONTAINER_NAME sh" + log_info " Web前端日志: docker exec $WEB_CONTAINER_NAME tail -f /var/log/nuxt.log" log_info " 停止服务: $DOCKER_COMPOSE_CMD -f $DOCKER_COMPOSE_FILE down" log_info " 重启服务: $DOCKER_COMPOSE_CMD -f $DOCKER_COMPOSE_FILE restart" log_info " 重新构建: $0 --rebuild" @@ -2325,7 +2363,7 @@ main() { echo "==========================================" echo " ECShopX 开发环境设置 (Docker)" - echo " 支持: ECShopX / ECShopX_admin-frontend / ECShopX_mobile-frontend / ECShopX_desktop-frontend" + echo " 支持: ECShopX / ECShopX_admin-frontend / ECShopX_mobile-frontend / ECShopX_web-frontend" echo "==========================================" echo "" @@ -2346,9 +2384,9 @@ main() { configure_cron_and_supervisor_queues # 编译前端项目 + build_pc || log_warning "PC商城(ECShopX_web-frontend)编译未完成" build_admin || log_warning "管理后台(ECShopX_admin-frontend)编译未完成" build_vshop || log_warning "移动商城(ECShopX_mobile-frontend)编译未完成" - build_pc || log_warning "PC商城(ECShopX_desktop-frontend)编译未完成" # 导入 Demo 数据 import_demo_data || log_warning "Demo 数据导入未完成" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 73724e0..9ea7909 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -50,6 +50,19 @@ services: networks: - ecshopx-dev-network + ecshopx-web-frontend: + image: node:20.19.0-alpine + container_name: ecshopx-web-frontend + restart: unless-stopped + working_dir: /data/httpd/ECShopX_web-frontend + command: sh -c "tail -f /dev/null" + volumes: + - ..:/data/httpd + environment: + - TZ=Asia/Shanghai + networks: + - ecshopx-dev-network + volumes: mysql_data: name: ecshopx-dev-mysql diff --git a/docker-dev/Dockerfile b/docker-dev/Dockerfile index e94ae4b..161f8f8 100644 --- a/docker-dev/Dockerfile +++ b/docker-dev/Dockerfile @@ -68,7 +68,7 @@ RUN mkdir -p /var/log/supervisor \ && mkdir -p /data/httpd/ECShopX \ && mkdir -p /data/httpd/ECShopX_admin-frontend \ && mkdir -p /data/httpd/ECShopX_mobile-frontend \ - && mkdir -p /data/httpd/ECShopX_desktop-frontend \ + && mkdir -p /data/httpd/ECShopX_web-frontend \ && (id -u redis >/dev/null 2>&1 || adduser -D -s /sbin/nologin redis) \ && (id -u www-data >/dev/null 2>&1 || (addgroup -g 82 www-data && adduser -D -s /sbin/nologin -u 82 -G www-data www-data)) \ && chown -R redis:redis /var/lib/redis /var/log/redis /var/run/redis \ diff --git a/docker-dev/nginx.conf b/docker-dev/nginx.conf index 229223b..289c516 100644 --- a/docker-dev/nginx.conf +++ b/docker-dev/nginx.conf @@ -170,9 +170,9 @@ http { listen 8082; server_name _; - # 代理到Nuxt服务(监听3000端口) + # 代理到独立的 Web 前端 Nuxt 服务(监听3000端口) location / { - proxy_pass http://127.0.0.1:3000; + proxy_pass http://ecshopx-web-frontend:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; diff --git a/docker-dev/supervisord.conf b/docker-dev/supervisord.conf index 42a5852..2f3ab77 100644 --- a/docker-dev/supervisord.conf +++ b/docker-dev/supervisord.conf @@ -65,19 +65,6 @@ stderr_logfile=/var/log/supervisor/nginx.log user=root priority=400 -[program:nuxt] -command=/bin/sh -c "sleep 5; cd /data/httpd/ECShopX_desktop-frontend && if [ -d .nuxt ] || [ -d .output ]; then if npm run 2>/dev/null | grep -q 'start'; then PORT=3000 HOST=0.0.0.0 npm run start; else PORT=3000 HOST=0.0.0.0 npm run dev; fi; else exit 0; fi" -autostart=true -autorestart=true -startsecs=10 -startretries=3 -stdout_logfile=/var/log/supervisor/nuxt.log -stderr_logfile=/var/log/supervisor/nuxt.log -user=root -priority=500 -directory=/data/httpd/ECShopX_desktop-frontend - - [program:crond] command=/usr/sbin/crond -f -l 8 autostart=true diff --git a/routes/api/member.php b/routes/api/member.php index d56c2fd..483d45a 100644 --- a/routes/api/member.php +++ b/routes/api/member.php @@ -39,6 +39,7 @@ $api->version('v1', function($api) { $api->get('/member/sms/code', ['as' => 'member.sms.code', 'uses' => 'Members@getSmsCode']); // 获取图片验证码-已支持h5 $api->get('/member/image/code', ['as' => 'member.image.code', 'uses' => 'Members@getImageVcode']); + $api->get('/member/verify', ['name'=>'POS会员手机号验证码校验并查询会员信息','middleware'=>['activated'], 'as' => 'member.verify', 'uses' =>'Members@verifyMember']); $api->post('/member', ['name'=>'新增会员','middleware'=>['activated'], 'as' => 'member.create', 'uses' =>'Members@createMember']); $api->patch('/member', ['name'=>'更新会员信息','middleware'=>'activated', 'as' => 'member.update', 'uses' =>'Members@updateMemberInfo']); $api->put('/member', ['name'=>'更新会员手机信息','middleware'=>'activated', 'as' => 'member.upate.mobile', 'uses' =>'Members@updateMobileById']); diff --git a/routes/frontapi/goods.php b/routes/frontapi/goods.php index 020cf26..ec488e5 100644 --- a/routes/frontapi/goods.php +++ b/routes/frontapi/goods.php @@ -16,6 +16,8 @@ $api->version('v1', function ($api) { $api->group(['namespace' => 'GoodsBundle\Http\FrontApi\V1\Action', 'middleware' => 'frontnoauth:h5app', 'prefix' => 'h5app'], function ($api) { // 商品列表-已支持h5 $api->get('/wxapp/goods/items', ['as' => 'goods.items.lists', 'uses' => 'Items@getItemsList']); + // 批量获取商品基本信息(图片、ID、名称、价格) + $api->get('/wxapp/goods/items/batch', ['as' => 'goods.items.batch', 'uses' => 'Items@getBatchItems']); // 获取商品筛选条件 $api->get('/wxapp/goods/items/filter', ['as' => 'goods.items.filter', 'uses' => 'Items@getItemsFilter']); //获取小店商品列表 diff --git a/src/DistributionBundle/Http/FrontApi/V1/Action/Distributor.php b/src/DistributionBundle/Http/FrontApi/V1/Action/Distributor.php index 1c9c738..7205f5c 100644 --- a/src/DistributionBundle/Http/FrontApi/V1/Action/Distributor.php +++ b/src/DistributionBundle/Http/FrontApi/V1/Action/Distributor.php @@ -795,8 +795,6 @@ class Distributor extends BaseController $filter = []; - // $filter['is_valid'] = 'true'; - $type = $request->input('type', 0); // 过滤条件 $noHaving = false; // 是否过滤离用户的经纬度比较远的店铺 【true 不过滤】【false 过滤】 switch ($type) { @@ -927,11 +925,11 @@ class Distributor extends BaseController // 默认拿不是总店的店铺 $filter['distributor_self'] = 0; - // $filter['is_valid'] = 'true'; + // 默认只返回启用的店铺,可通过 is_valid 参数显式筛选 if ($request->input('is_valid')) { $filter['is_valid'] = $request->input('is_valid'); } else { - $filter['is_valid'] = ['true', 'false']; + $filter['is_valid'] = 'true'; } if ($request->input('get_shop')) { diff --git a/src/EmployeePurchaseBundle/Http/FrontApi/V1/Action/StoreHomePage.php b/src/EmployeePurchaseBundle/Http/FrontApi/V1/Action/StoreHomePage.php index 3267039..1248f94 100644 --- a/src/EmployeePurchaseBundle/Http/FrontApi/V1/Action/StoreHomePage.php +++ b/src/EmployeePurchaseBundle/Http/FrontApi/V1/Action/StoreHomePage.php @@ -21,7 +21,7 @@ class StoreHomePage extends BaseController * path="/wxapp/employeepurchase/store-home-page/{id}", * summary="内购模版详情(含完整模板装修数据)", * tags={"内购"}, - * description="返回内购模版表字段、pages_template 列表完整行(pages_template_record)、以及与 pagestemplate/detail 同构的 page_template_detail(list/config/tab_bar 等)。当存在 weapp_customize_page_id 时,page_template_detail 优先从 wechat_weapp_setting 的 page_name=custom_{weapp_customize_page_id}、version=v1.0.1 读取(与后台 shopDecoration 保存一致);若无匹配行则回退为商城首页 index(v1.0.2)。resolved_pages_template_id 为装修 pages_template 主键,与自定义页 page_name 语义不同。需传 distributor_id。可选 e_activity_id 参与组件价活动上下文。", + * description="返回内购模版表字段、pages_template 列表完整行(pages_template_record)、以及与 pagestemplate/detail 同构的 page_template_detail(list/config/tab_bar 等)。当存在 weapp_customize_page_id 时,page_template_detail 从 wechat_weapp_setting 的 page_name=custom_{weapp_customize_page_id} 读取,version 依次尝试 shop_{distributor_id} 与 v1.0.1。resolved_pages_template_id 为装修 pages_template 主键,与自定义页 page_name 语义不同。可选 distributor_id、e_activity_id。", * operationId="employeepurchaseStoreHomePageDetailFront", * @SWG\Parameter(name="Authorization", in="header", description="JWT验证token", required=true, type="string"), * @SWG\Parameter(name="id", in="path", description="内购模版主键 employee_purchase_store_home_page.id", required=true, type="integer"), diff --git a/src/EmployeePurchaseBundle/Services/StoreHomePageService.php b/src/EmployeePurchaseBundle/Services/StoreHomePageService.php index bdb4ab9..12c49f3 100644 --- a/src/EmployeePurchaseBundle/Services/StoreHomePageService.php +++ b/src/EmployeePurchaseBundle/Services/StoreHomePageService.php @@ -63,6 +63,66 @@ class StoreHomePageService return 'custom_'.$weappCustomizePageId; } + /** + * 自定义页装修 version 候选(按优先级)。 + * - decorate/index scene=1010:门店维度写入 shop_{distributor_id} + * - 旧 shopDecoration page_template:固定 v1.0.1(与 distributor 无关) + * + * @return list + */ + public static function customDecorationSettingVersionCandidates(int $rowDistributorId, int $authDistributorId): array + { + $distributorId = $rowDistributorId > 0 ? $rowDistributorId : $authDistributorId; + $versions = []; + if ($distributorId > 0) { + $versions[] = 'shop_'.$distributorId; + } + $versions[] = self::CUSTOM_DECORATION_SETTING_VERSION; + + return array_values(array_unique($versions)); + } + + /** + * 拉取 wechat_weapp_setting 时尝试的 template_name(decorate/index scene=1010 曾硬编码 yykweishop)。 + * + * @param array $storeHomeRow + * + * @return list + */ + public static function decorationTemplateNameCandidates(array $storeHomeRow): array + { + $names = []; + $primary = (string) ($storeHomeRow['template_name'] ?? ''); + if ($primary !== '') { + $names[] = $primary; + } + if ($primary !== 'yykweishop') { + $names[] = 'yykweishop'; + } + + return array_values(array_unique($names)); + } + + /** + * 解析 pages_template 列表时的查询计划(按优先级)。 + * + * @return list + */ + public static function pagesTemplateListSearchPlans(int $rowDistributorId): array + { + if ($rowDistributorId > 0) { + return [ + ['distributor_id' => $rowDistributorId, 'weapp_pages' => 'distributor_index'], + ['distributor_id' => $rowDistributorId, 'weapp_pages' => 'index'], + ['distributor_id' => 0, 'weapp_pages' => 'index'], + ]; + } + + return [ + ['distributor_id' => 0, 'weapp_pages' => 'index'], + ]; + } + /** * @param array|null $detail PagesTemplateServices::content 返回值 */ @@ -253,10 +313,11 @@ class StoreHomePageService 'page_template_detail' => null, ]; + $templateName = (string) ($row['template_name'] ?? ''); $pid = (int) ($resolved['resolved_pages_template_id'] ?? 0); - if ($pid > 0) { - $distId = (int) ($row['distributor_id'] ?? 0); - $customizeId = isset($row['weapp_customize_page_id']) ? (int) $row['weapp_customize_page_id'] : 0; + $distId = (int) ($row['distributor_id'] ?? 0); + $customizeId = isset($row['weapp_customize_page_id']) ? (int) $row['weapp_customize_page_id'] : 0; + if ($templateName !== '' && ($pid > 0 || $customizeId > 0)) { $pagesTemplateServices = new PagesTemplateServices(); $indexParams = [ @@ -264,8 +325,8 @@ class StoreHomePageService 'regionauth_id' => 0, 'user_id' => $userId, 'distributor_id' => $distId, - 'weapp_pages' => 'index', - 'template_name' => (string) ($row['template_name'] ?? ''), + 'weapp_pages' => $distId > 0 ? 'distributor_index' : 'index', + 'template_name' => $templateName, 'version' => self::INDEX_DECORATION_SETTING_VERSION, 'page' => '1', 'page_size' => '50', @@ -277,26 +338,14 @@ class StoreHomePageService $customPageName = self::weappSettingPageNameForCustomizePage($customizeId); if ($customPageName !== null) { - $customParams = array_merge($indexParams, [ - 'version' => self::CUSTOM_DECORATION_SETTING_VERSION, - 'weapp_setting_page_name' => $customPageName, - ]); - $detail = $pagesTemplateServices->content($customParams); - if (!self::pageTemplateDetailHasNonEmptyList($detail)) { - $customParams['weapp_setting_pages_template_id'] = 0; - $detail = $pagesTemplateServices->content($customParams); - } - if (!self::pageTemplateDetailHasNonEmptyList($detail)) { - app('log')->warning('[StoreHomePageService] enterprise_store_home 自定义页装修无匹配 wechat_weapp_setting,回退 index', [ - 'company_id' => $companyId, - 'store_home_page_id' => $id, - 'weapp_customize_page_id' => $customizeId, - 'weapp_setting_page_name' => $customPageName, - ]); - $detail = $pagesTemplateServices->content($indexParams); - } - $base['page_template_detail'] = $detail; - } else { + $base['page_template_detail'] = $this->fetchCustomPageDecorationDetail( + $companyId, + $row, + $customPageName, + $distId, + $authDistributorId + ); + } elseif ($pid > 0) { $base['page_template_detail'] = $pagesTemplateServices->content($indexParams); } } @@ -325,19 +374,25 @@ class StoreHomePageService } $distributorId = (int) ($storeHomeRow['distributor_id'] ?? 0); - $weappPages = $distributorId > 0 ? 'distributor_index' : 'index'; $pagesTemplateServices = new PagesTemplateServices(); - $listResult = $pagesTemplateServices->lists([ - 'company_id' => $companyId, - 'distributor_id' => $distributorId, - 'weapp_pages' => $weappPages, - 'page_no' => 1, - 'page_size' => 100, - ]); - - $rows = $listResult['list'] ?? []; - $picked = self::pickResolvedPagesTemplateRow(is_array($rows) ? $rows : [], $templateName); + $picked = null; + foreach (self::decorationTemplateNameCandidates($storeHomeRow) as $tryTemplateName) { + foreach (self::pagesTemplateListSearchPlans($distributorId) as $plan) { + $listResult = $pagesTemplateServices->lists([ + 'company_id' => $companyId, + 'distributor_id' => $plan['distributor_id'], + 'weapp_pages' => $plan['weapp_pages'], + 'page_no' => 1, + 'page_size' => 100, + ]); + $rows = $listResult['list'] ?? []; + $picked = self::pickResolvedPagesTemplateRow(is_array($rows) ? $rows : [], $tryTemplateName); + if ($picked !== null) { + break 2; + } + } + } if ($picked === null) { return [ @@ -361,6 +416,122 @@ class StoreHomePageService ]; } + /** + * 自定义页装修:与后台 getParamByTempName 一致(custom_{id} + shop_{distributor_id},pages_template_id=0)。 + * + * @param array $storeHomeRow + * + * @return array{list: array>, config: array>} + */ + private function fetchCustomPageDecorationDetail( + int $companyId, + array $storeHomeRow, + string $customPageName, + int $distId, + int $authDistributorId + ): array { + foreach (self::decorationTemplateNameCandidates($storeHomeRow) as $tryTemplateName) { + foreach (self::customDecorationSettingVersionCandidates($distId, $authDistributorId) as $version) { + $entities = $this->weappSettingRepository->getParamByTempName( + $companyId, + $tryTemplateName, + $customPageName, + null, + $version, + 0 + ); + $list = self::buildTemplateConfListFromWeappSettingEntities($entities, $companyId, $tryTemplateName); + if ($list !== []) { + return self::buildPageTemplateDetailFromTemplateConfList($list); + } + } + } + + return ['list' => [], 'config' => []]; + } + + /** + * @param mixed $raw + * + * @return array + */ + public static function safeDecodeWeappSettingParams($raw): array + { + if (is_array($raw)) { + return $raw; + } + if (!is_string($raw) || $raw === '') { + return []; + } + + $unserialized = @unserialize($raw); + if (is_array($unserialized)) { + return $unserialized; + } + + $decoded = json_decode($raw, true); + + return is_array($decoded) ? $decoded : []; + } + + /** + * @param mixed $entities + * + * @return array> + */ + public static function buildTemplateConfListFromWeappSettingEntities($entities, int $companyId, string $fallbackTemplateName = ''): array + { + if ($entities instanceof \Traversable) { + $entities = iterator_to_array($entities); + } + if (!is_array($entities)) { + return []; + } + + $list = []; + foreach ($entities as $row) { + if (!is_object($row)) { + continue; + } + + $pageName = method_exists($row, 'getPageName') ? (string) $row->getPageName() : ''; + $list[] = [ + 'id' => method_exists($row, 'getId') ? $row->getId() : 0, + 'template_name' => method_exists($row, 'getTemplateName') ? $row->getTemplateName() : $fallbackTemplateName, + 'company_id' => method_exists($row, 'getCompanyId') ? $row->getCompanyId() : $companyId, + 'name' => method_exists($row, 'getName') ? (string) $row->getName() : '', + 'page_name' => $pageName !== '' ? $pageName : 'index', + 'params' => self::safeDecodeWeappSettingParams(method_exists($row, 'getParams') ? $row->getParams() : null), + ]; + } + + return $list; + } + + /** + * @param array> $list + * + * @return array + */ + public static function buildPageTemplateDetailFromTemplateConfList(array $list): array + { + $config = []; + foreach ($list as $row) { + if (!is_array($row)) { + continue; + } + $params = $row['params'] ?? null; + if (is_array($params) && isset($params['name']) && isset($params['base'])) { + $config[] = $params; + } + } + + return [ + 'list' => $list, + 'config' => $config, + ]; + } + /** * 从 pages_template 列表中选出与内购模版 template_name 一致且启用的记录;多条时取列表顺序第一条。 * diff --git a/src/GoodsBundle/Http/FrontApi/V1/Action/Category.php b/src/GoodsBundle/Http/FrontApi/V1/Action/Category.php index abd9c9f..1164a09 100644 --- a/src/GoodsBundle/Http/FrontApi/V1/Action/Category.php +++ b/src/GoodsBundle/Http/FrontApi/V1/Action/Category.php @@ -18,6 +18,7 @@ namespace GoodsBundle\Http\FrontApi\V1\Action; use App\Http\Controllers\Controller as BaseController; +use CompanysBundle\Ego\CompanysActivationEgo; use DistributionBundle\Services\DistributorService; use GoodsBundle\Services\ItemsCategoryService; use GoodsBundle\Services\ItemsRelCatsService; @@ -138,8 +139,13 @@ class Category extends BaseController // 小程序端仅返回前台展示的分类 $filter['is_show_front'] = 1; - if ($request->input('distributor_id')) { - // $filter['distributor_id'] = $request->input('distributor_id'); + $distributorId = (int)$request->input('distributor_id', 0); + $company = (new CompanysActivationEgo())->check($company_id); + $productModel = $company['product_model'] ?? 'platform'; + $itemsCategoryService = new ItemsCategoryService(); + $categoryDistributorId = $itemsCategoryService->resolveCategoryDistributorIdForFront($productModel, $distributorId); + if ($productModel !== 'standard' && $categoryDistributorId > 0) { + $filter['distributor_id'] = $categoryDistributorId; } $onlyTop = $request->input('only_top', false); @@ -148,7 +154,6 @@ class Category extends BaseController $filter['category_level'] = 1; } - $itemsCategoryService = new ItemsCategoryService(); $result = $itemsCategoryService->getItemsCategory($filter, true, 1, -1, ['sort' => 'DESC', 'created' => 'ASC'], 'category_id,category_name,category_level,parent_id,image_url,customize_page_id'); // 分类获取不到获取商城主类目 @@ -180,8 +185,9 @@ class Category extends BaseController } } - $distributorId = (int)$request->input('distributor_id', 0); - if ($distributorId > 0 && $itemsCategoryService->isSaleableCategoryFilterEnabled($company_id)) { + $applySaleableFilter = $itemsCategoryService->shouldApplySaleableFilterForFront($productModel, $distributorId) + || ($distributorId > 0 && $itemsCategoryService->isSaleableCategoryFilterEnabled($company_id)); + if ($distributorId > 0 && $applySaleableFilter) { $result = $itemsCategoryService->filterCategoryTreeBySaleableItems($company_id, $distributorId, $result); } @@ -444,13 +450,26 @@ class Category extends BaseController $filter['company_id'] = $company_id; - if ($request->input('distributor_id')) { - $filter['distributor_id'] = $request->input('distributor_id'); + $distributorId = (int)$request->input('distributor_id', 0); + $company = (new CompanysActivationEgo())->check($company_id); + $productModel = $company['product_model'] ?? 'platform'; + $itemsCategoryService = new ItemsCategoryService(); + $categoryDistributorId = $itemsCategoryService->resolveCategoryDistributorIdForFront($productModel, $distributorId); + if ($productModel !== 'standard' && $categoryDistributorId > 0) { + $filter['distributor_id'] = $categoryDistributorId; } $filter['is_main_category'] = $request->input('is_main_category', false); -// $filter['category_level'] = $request->input('category_level'); + if ($itemsCategoryService->shouldApplySaleableFilterForFront($productModel, $distributorId)) { + $filter['category_id'] = $itemsCategoryService->getSaleableTopLevelCategoryIds($company_id, $distributorId); + if (empty($filter['category_id'])) { + return $this->response->array(['list' => [], 'total_count' => 0]); + } + $result = $itemsCategoryService->lists($filter); + + return $this->response->array($result); + } $settingService = new SettingService(); $config = $settingService->getConfig($company_id); @@ -463,14 +482,18 @@ class Category extends BaseController $itemFilter['item_type'] = 'normal'; $itemFilter['is_default'] = true; - $distributorFilter = [ - 'company_id' => $company_id, - 'is_valid' => 'true' - ]; - $distributorService = new DistributorService(); - $validDistributorList = $distributorService->getDistributorOriginalList($distributorFilter, 1, -1); - $validDistributorIds = array_column($validDistributorList['list'], 'distributor_id'); - $itemFilter['distributor_id'] = array_merge(['0'], $validDistributorIds); + if ($categoryDistributorId > 0) { + $itemFilter['distributor_id'] = $categoryDistributorId; + } else { + $distributorFilter = [ + 'company_id' => $company_id, + 'is_valid' => 'true' + ]; + $distributorService = new DistributorService(); + $validDistributorList = $distributorService->getDistributorOriginalList($distributorFilter, 1, -1); + $validDistributorIds = array_column($validDistributorList['list'], 'distributor_id'); + $itemFilter['distributor_id'] = array_merge(['0'], $validDistributorIds); + } $itemsService = new ItemsService(); $itemsList = $itemsService->itemsRepository->list($itemFilter, [], -1, 1, ['item_id']); @@ -480,7 +503,6 @@ class Category extends BaseController $itemsRelCatsService = new ItemsRelCatsService(); $itemsRelCatsList = $itemsRelCatsService->lists($itemRelCatsParams); - $itemsCategoryService = new ItemsCategoryService(); $filter['category_id'] = []; foreach ($itemsRelCatsList['list'] as $cat) { $category = $itemsCategoryService->getInfo(['company_id' => $company_id, 'category_id' => $cat['category_id']]); @@ -495,7 +517,6 @@ class Category extends BaseController } } - $itemsCategoryService = new ItemsCategoryService(); $result = $itemsCategoryService->lists($filter); return $this->response->array($result); diff --git a/src/GoodsBundle/Http/FrontApi/V1/Action/Items.php b/src/GoodsBundle/Http/FrontApi/V1/Action/Items.php index 62ae699..3132c37 100644 --- a/src/GoodsBundle/Http/FrontApi/V1/Action/Items.php +++ b/src/GoodsBundle/Http/FrontApi/V1/Action/Items.php @@ -1498,4 +1498,54 @@ class Items extends BaseController unset($itemInfo['itemId'], $itemInfo['consumeType'], $itemInfo['itemName'], $itemInfo['itemBn'], $itemInfo['companyId'], $itemInfo['item_main_cat_id'], $itemInfo['nospec'], $itemInfo['pics_create_qrcode']); return $this->response->array($itemInfo); } + + /** + * @SWG\Get( + * path="/wxapp/goods/items/batch", + * summary="批量获取商品基本信息", + * tags={"商品"}, + * description="批量获取商品基本信息(图片、ID、名称、价格),供 ecshopx-web 等端使用", + * operationId="getBatchItems", + * @SWG\Parameter( name="Authorization", in="header", description="JWT验证token", type="string" ), + * @SWG\Parameter( name="item_ids", in="query", description="商品ID列表,逗号分隔,如 1,2,3", required=true, type="string" ), + * @SWG\Response( + * response=200, + * description="成功返回结构", + * @SWG\Schema( + * @SWG\Property( + * property="data", + * type="array", + * @SWG\Items( + * type="object", + * @SWG\Property(property="item_id", type="integer", description="商品ID"), + * @SWG\Property(property="item_name", type="string", description="商品名称"), + * @SWG\Property(property="price", type="integer", description="销售价(单位:分)"), + * @SWG\Property(property="pics", type="array", description="商品图片数组", @SWG\Items(type="string")) + * ) + * ) + * ) + * ), + * @SWG\Response( response="default", description="错误返回结构", @SWG\Schema( type="array", @SWG\Items(ref="#/definitions/GoodsErrorRespones") ) ) + * ) + */ + public function getBatchItems(Request $request) + { + $authInfo = $request->get('auth'); + $itemIdsStr = $request->input('item_ids', ''); + + if (empty($itemIdsStr)) { + return $this->response->array([]); + } + + $itemIds = array_filter(array_map('intval', explode(',', $itemIdsStr))); + if (empty($itemIds)) { + return $this->response->array([]); + } + + $itemsRepository = app('registry')->getManager('default')->getRepository(\GoodsBundle\Entities\Items::class); + $cols = 'item_id,item_name,price,pics'; + $result = $itemsRepository->getLists(['item_id' => $itemIds], $cols, 1, -1, []); + + return $this->response->array($result); + } } diff --git a/src/GoodsBundle/Services/ItemsCategoryService.php b/src/GoodsBundle/Services/ItemsCategoryService.php index 1803136..6c60716 100644 --- a/src/GoodsBundle/Services/ItemsCategoryService.php +++ b/src/GoodsBundle/Services/ItemsCategoryService.php @@ -144,6 +144,66 @@ class ItemsCategoryService return 'goods:category:saleable_filter:' . $companyId; } + /** + * FrontApi 分类列表查询用的 distributor_id(items_category 维度)。 + * BBC(standard) 共用平台销售分类;platform 使用店铺独立分类。 + */ + public function resolveCategoryDistributorIdForFront(string $productModel, int $requestDistributorId): int + { + if ($productModel === 'standard') { + return 0; + } + + return $requestDistributorId; + } + + /** + * FrontApi 是否按店铺可售商品过滤分类树(BBC 店铺主页)。 + */ + public function shouldApplySaleableFilterForFront(string $productModel, int $requestDistributorId): bool + { + return $productModel === 'standard' && $requestDistributorId > 0; + } + + /** + * 获取店铺可售商品对应的一级销售分类 ID 列表(用于 categorylevel 等扁平接口)。 + */ + public function getSaleableTopLevelCategoryIds(int $companyId, int $distributorId): array + { + if ($companyId <= 0 || $distributorId <= 0) { + return []; + } + + $categoryIdMap = $this->getSaleableCategoryIdMap($companyId, $distributorId); + if (empty($categoryIdMap)) { + return []; + } + + $topLevelIds = []; + foreach (array_keys($categoryIdMap) as $categoryId) { + if (!is_numeric($categoryId)) { + continue; + } + $category = $this->itemsCategoryRepository->getInfo([ + 'company_id' => $companyId, + 'category_id' => (int)$categoryId, + ]); + if (!$category) { + continue; + } + if ((int)($category['parent_id'] ?? 0) === 0) { + $topLevelIds[] = (int)$category['category_id']; + } else { + $path = explode(',', (string)($category['path'] ?? '')); + if (!empty($path[0]) && is_numeric($path[0])) { + $topLevelIds[] = (int)$path[0]; + } + } + } + + return array_values(array_unique($topLevelIds)); + } + /** * 关于节点的回溯 * @param $categories diff --git a/src/KaquanBundle/Http/Api/V1/Action/DiscountCard.php b/src/KaquanBundle/Http/Api/V1/Action/DiscountCard.php index 844208a..1b79a1a 100644 --- a/src/KaquanBundle/Http/Api/V1/Action/DiscountCard.php +++ b/src/KaquanBundle/Http/Api/V1/Action/DiscountCard.php @@ -29,6 +29,7 @@ use KaquanBundle\Services\DiscountNewGiftCardService; use KaquanBundle\Services\KaquanService; use KaquanBundle\Services\DiscountCardService; use KaquanBundle\Services\UserDiscountService; +use KaquanBundle\Support\ShopDiscountCardListFilter; class DiscountCard extends BaseController { @@ -656,15 +657,12 @@ class DiscountCard extends BaseController } $store_self = $request->input('store_self'); - $sourceId = floatval($request->get('distributor_id', 0));//如果是平台,这里是0 + $sourceId = intval($request->get('distributor_id', 0));//如果是平台,这里是0 if ($store_self == "true") {//平台版仅支持自营商品【总店】 $filter['or']['distributor_id|like'] = ',0,'; $filter['or']['distributor_id|like'] = '%,%'; - } else { - if ($request->get('distributor_id')) { - $filter['or']['distributor_id|like'] = ',' . $request->get('distributor_id') . ','; - $filter['or']['distributor_id|like'] = '%,%'; - } + } elseif ($sourceId > 0) { + ShopDiscountCardListFilter::applyToFilter($filter, $sourceId); } if ($request->input('receive')) { @@ -672,10 +670,6 @@ class DiscountCard extends BaseController } if ($from == 'btn') { - // 如果来源是按钮出发,平台显示所有的券,店铺显示自己的券 - if ($sourceId > 0) { - $filter['source_id'] = $sourceId; - } $filter['end_date'] = time();//排除已过期的优惠券 } diff --git a/src/KaquanBundle/Support/ShopDiscountCardListFilter.php b/src/KaquanBundle/Support/ShopDiscountCardListFilter.php new file mode 100644 index 0000000..49ec752 --- /dev/null +++ b/src/KaquanBundle/Support/ShopDiscountCardListFilter.php @@ -0,0 +1,77 @@ + $filter + */ + public static function applyToFilter(array &$filter, int $distributorId): bool + { + if ($distributorId <= 0) { + return false; + } + + $filter['or'] = self::orConditionsForShop($distributorId); + + return true; + } + + /** + * @return array + */ + public static function orConditionsForShop(int $distributorId): array + { + return [ + 'source_id' => $distributorId, + 'use_all_shops' => 1, + // DiscountCardsRepository::__orFilter 的 like 不会自动包裹 % + 'distributor_id|like' => '%,' . $distributorId . ',%', + 'distributor_id' => ',', + ]; + } + + /** + * @param array $coupon + */ + public static function matchesCoupon(array $coupon, int $distributorId): bool + { + if ($distributorId <= 0) { + return true; + } + + if ((int) ($coupon['source_id'] ?? 0) === $distributorId) { + return true; + } + + if (!empty($coupon['use_all_shops']) && (int) $coupon['use_all_shops'] === 1) { + return true; + } + + $distributorIdField = (string) ($coupon['distributor_id'] ?? ''); + if ($distributorIdField === ',') { + return true; + } + + return str_contains($distributorIdField, ',' . $distributorId . ','); + } +} diff --git a/src/MembersBundle/Http/Api/V1/Action/Members.php b/src/MembersBundle/Http/Api/V1/Action/Members.php index c7958f1..39d5771 100644 --- a/src/MembersBundle/Http/Api/V1/Action/Members.php +++ b/src/MembersBundle/Http/Api/V1/Action/Members.php @@ -1515,9 +1515,63 @@ class Members extends Controller if (!$memberRegSettingService->checkImageVcode($token, $companyId, $yzmcode, $type)) { throw new ResourceException(trans('MembersBundle/Members.image_captcha_error')); } - $memberRegSettingService->generateSmsVcode($mobile, $companyId, $type); + $debugVcode = $memberRegSettingService->generateSmsVcode($mobile, $companyId, $type); - return $this->response->array(['status' => true]); + $response = ['status' => true]; + if (is_string($debugVcode)) { + $response['message'] = '短信调试模式已开启'; + $response['debug_vcode'] = $debugVcode; + } + return $this->response->array($response); + } + + /** + * @SWG\Get( + * path="/member/verify", + * summary="POS 会员手机号验证码校验并查询会员信息", + * tags={"会员"}, + * description="POS 场景通过手机号和短信验证码校验会员身份,并返回会员信息,不生成前台会员登录态", + * operationId="verifyMemberByMobileCode", + * @SWG\Parameter(name="mobile", in="query", description="手机号", required=true, type="string"), + * @SWG\Parameter(name="vcode", in="query", description="短信验证码", required=true, type="string"), + * @SWG\Response(response=200, description="成功返回结构"), + * @SWG\Response(response="default", description="错误返回结构", @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/MembersErrorRespones"))) + * ) + */ + public function verifyMember(Request $request) + { + $companyId = (int) app('auth')->user()->get('company_id'); + $mobile = trim((string) $request->input('mobile', '')); + $vcode = trim((string) $request->input('vcode', '')); + + if ($mobile === '' || !preg_match('/^1[3456789]\d{9}$/', $mobile)) { + throw new ResourceException(trans('MembersBundle/Members.mobile_error')); + } + + if ($vcode === '') { + throw new ResourceException(trans('MembersBundle/Members.verification_code_required')); + } + + if (!(new MemberRegSettingService())->checkSmsVcode($mobile, $companyId, $vcode, 'login')) { + throw new ResourceException(trans('MembersBundle/Members.sms_code_error')); + } + + $memberInfo = $this->memberService->getInfoByMobile($companyId, $mobile); + if (empty($memberInfo['user_id'])) { + throw new ResourceException(trans('MembersBundle/Members.mobile_not_exists')); + } + + return $this->response->array([ + 'data' => [ + 'user_id' => $memberInfo['user_id'], + 'mobile' => $memberInfo['mobile'] ?? $mobile, + 'username' => $memberInfo['username'] ?? '', + 'user_card_code' => $memberInfo['user_card_code'] ?? '', + 'avatar' => $memberInfo['avatar'] ?? '', + 'member_name' => $memberInfo['username'] ?? '', + 'member_mobile' => $memberInfo['mobile'] ?? $mobile, + ], + ]); } public function createMember(Request $request) @@ -1531,13 +1585,13 @@ class Members extends Controller throw new ResourceException(trans('MembersBundle/Members.invalid_mobile')); } - /*if (!$postData['vcode']) { + if (empty($postData['vcode'])) { throw new ResourceException(trans('MembersBundle/Members.verification_code_required')); } if (!(new MemberRegSettingService())->checkSmsVcode($postData['mobile'], $companyId, $postData['vcode'], $postData['check_type'] ?? 'sign')) { throw new ResourceException(trans('MembersBundle/Members.sms_code_error')); - }*/ + } $memberInfo = $this->memberService->getInfoByMobile((int)$companyId, (string)$postData['mobile']); if ($memberInfo) { @@ -1559,7 +1613,7 @@ class Members extends Controller //新增-会员信息 $memberInfo = [ 'company_id' => $companyId, - 'username' => randValue(8), + 'username' => $postData['name'] ?? randValue(8), 'mobile' => $postData['mobile'], 'grade_id' => $defaultGradeInfo['grade_id'], 'password' => substr(str_shuffle('QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm'), 5, 10), diff --git a/src/MembersBundle/Http/FrontApi/V1/Action/Members.php b/src/MembersBundle/Http/FrontApi/V1/Action/Members.php index 53f1f88..968fba6 100644 --- a/src/MembersBundle/Http/FrontApi/V1/Action/Members.php +++ b/src/MembersBundle/Http/FrontApi/V1/Action/Members.php @@ -1247,8 +1247,13 @@ class Members extends Controller } } } - $memberRegSettingService->generateSmsVcode($phone, $companyId, $type); - return $this->response->array(['message' => "短信发送成功"]); + $debugVcode = $memberRegSettingService->generateSmsVcode($phone, $companyId, $type); + $response = ['message' => "短信发送成功"]; + if (is_string($debugVcode)) { + $response['message'] = '短信调试模式已开启'; + $response['debug_vcode'] = $debugVcode; + } + return $this->response->array($response); } /** diff --git a/src/MembersBundle/Services/MemberRegSettingService.php b/src/MembersBundle/Services/MemberRegSettingService.php index acc74c4..3e3fbe1 100644 --- a/src/MembersBundle/Services/MemberRegSettingService.php +++ b/src/MembersBundle/Services/MemberRegSettingService.php @@ -231,6 +231,10 @@ class MemberRegSettingService app('log')->info("code :" . json_encode(['phone' => $phone, 'company' => $companyId, 'vcode' => $vcode])); //保存验证码 $this->saveSmsVcode($phone, $companyId, $vcode, $type); + if (config("common.sms_debug_vcode")) { + app('log')->info("member sms debug vcode enabled :" . json_encode(['phone' => $phone, 'company' => $companyId, 'type' => $type])); + return $vcode; + } //发送短信 $this->sendSmsVcode($companyId, $phone, $vcode, $type); return true; diff --git a/src/OrdersBundle/Http/Api/V1/Action/Order.php b/src/OrdersBundle/Http/Api/V1/Action/Order.php index 26ac67c..a088e36 100644 --- a/src/OrdersBundle/Http/Api/V1/Action/Order.php +++ b/src/OrdersBundle/Http/Api/V1/Action/Order.php @@ -1875,7 +1875,7 @@ class Order extends Controller $setting['latest_aftersale_time'] = $input['latest_aftersale_time'] ?? 0; //默认确认收货后不可申请售后 $setting['auto_refuse_time'] = $input['auto_refuse_time'] ?? 0; //默认确认收货后不可申请售后 $setting['auto_aftersales'] = isset($input['auto_aftersales']) && $input['auto_aftersales'] && $input['auto_aftersales'] != 'false'; // 未发货售后自动同意 - $setting['offline_aftersales'] = isset($input['offline_aftersales']) && $input['offline_aftersales'] && $input['offline_aftersales'] != 'false'; // 到店退货 + $setting['offline_aftersales'] = isset($input['offline_aftersales']) && ($input['offline_aftersales'] === 'true' || $input['offline_aftersales'] === true); // 到店退货 $setting['is_refund_freight'] = $input['is_refund_freight'] ?? 0; // 退货退款可退运费 if ($setting['is_refund_freight'] == 1) { diff --git a/src/OrdersBundle/Services/Cart/DistributorCartObject.php b/src/OrdersBundle/Services/Cart/DistributorCartObject.php index dbfbfcc..bb883b3 100644 --- a/src/OrdersBundle/Services/Cart/DistributorCartObject.php +++ b/src/OrdersBundle/Services/Cart/DistributorCartObject.php @@ -48,7 +48,7 @@ class DistributorCartObject implements CartInterface public function checkItemParams($params) { // 检查是否是有效的会员优先购 - $memberpreference = $this->checkCurrentMemberpreferenceByItemId($params['company_id'], $params['user_id'], $params['item_id'], $params['shop_id'], false, $msg); + $memberpreference = $this->checkCurrentMemberpreferenceByItemId($params['company_id'], $params['user_id'], $params['item_id'], $msg, $params['shop_id'], false); if (!$memberpreference) { throw new ResourceException($msg); } diff --git a/src/OrdersBundle/Services/CartService.php b/src/OrdersBundle/Services/CartService.php index 6bcd9ac..511463a 100644 --- a/src/OrdersBundle/Services/CartService.php +++ b/src/OrdersBundle/Services/CartService.php @@ -662,7 +662,7 @@ class CartService $memberpreference = true; $cartdata['shop_type'] = $cartdata['shop_type'] ?? ''; if ($cartType != 'employee_purchase' && ($cartdata['shop_type'] != 'pointsmall' && $cartdata['shop_type'] != 'shop_offline')) { - $memberpreference = $this->checkCurrentMemberpreferenceByItemId($companyId, $userId, $itemId, $cartdata['shop_id'], false, $msg); + $memberpreference = $this->checkCurrentMemberpreferenceByItemId($companyId, $userId, $itemId, $msg, $cartdata['shop_id'], false); } if (!$memberpreference) { $invalidCart[] = $cartdata; diff --git a/src/OrdersBundle/Services/OrderService.php b/src/OrdersBundle/Services/OrderService.php index dc7b17d..5558d49 100644 --- a/src/OrdersBundle/Services/OrderService.php +++ b/src/OrdersBundle/Services/OrderService.php @@ -1370,7 +1370,7 @@ class OrderService $itemsCommissionService = new ItemsCommissionService(); foreach ($this->orderItemList as $itemInfo) { if (!in_array($this->orderInterface->orderClass, ['pointsmall', 'employee_purchase'])) { - $memberpreference = $this->checkCurrentMemberpreferenceByItemId($orderData['company_id'], $orderData['user_id'], $itemInfo['itemId'], $orderData['distributor_id'], false, $msg); + $memberpreference = $this->checkCurrentMemberpreferenceByItemId($orderData['company_id'], $orderData['user_id'], $itemInfo['itemId'], $msg, $orderData['distributor_id'], false); if (!$memberpreference) { $orderData['extraTips'] = $msg; if ($isCheck) { diff --git a/src/OrdersBundle/Traits/GetOrderIdTrait.php b/src/OrdersBundle/Traits/GetOrderIdTrait.php index bec3caf..bd3edbe 100644 --- a/src/OrdersBundle/Traits/GetOrderIdTrait.php +++ b/src/OrdersBundle/Traits/GetOrderIdTrait.php @@ -81,15 +81,30 @@ trait GetOrderIdTrait $identityOrderId = $promoterOrderId = []; $is_promoter_identity = $is_promoter_mobile = false; if (isset($filter['promoter_identity']) && $filter['promoter_identity']) { - $sql = "select promoter.user_id from popularize_promoter promoter left join popularize_promoter_identity identity on promoter.identity_id=identity.id where promoter.company_id=".$filter['company_id']." and identity.name='".$filter['promoter_identity']."'"; - $lists = $conn->executeQuery($sql)->fetchAll(); + $qb = $conn->createQueryBuilder(); + $lists = $qb->select('promoter.user_id') + ->from('popularize_promoter', 'promoter') + ->leftJoin('promoter', 'popularize_promoter_identity', 'identity', 'promoter.identity_id = identity.id') + ->where($qb->expr()->eq('promoter.company_id', $qb->expr()->literal($filter['company_id']))) + ->andWhere($qb->expr()->eq('identity.name', $qb->expr()->literal($filter['promoter_identity']))) + ->execute() + ->fetchAll(); $userIds = array_column($lists, 'user_id'); $userIds = array_filter($userIds, function($value) { return $value !== null && $value !== false && $value !== "" && $value !== 0; }); if ($userIds) { - $sql = "select id,order_id,user_id,buy_user_id from popularize_brokerage where company_id=".$filter['company_id']." and brokerage_type='first_level' and user_id in (".implode($userIds, ',').")"; - $lists = $conn->executeQuery($sql)->fetchAll(); + $qb = $conn->createQueryBuilder(); + $userIdLiterals = array_map(function ($value) use ($qb) { + return $qb->expr()->literal($value); + }, $userIds); + $lists = $qb->select('id', 'order_id', 'user_id', 'buy_user_id') + ->from('popularize_brokerage') + ->where($qb->expr()->eq('company_id', $qb->expr()->literal($filter['company_id']))) + ->andWhere($qb->expr()->eq('brokerage_type', $qb->expr()->literal('first_level'))) + ->andWhere($qb->expr()->in('user_id', $userIdLiterals)) + ->execute() + ->fetchAll(); $identityOrderId = array_column($lists, 'order_id'); $identityOrderId = array_filter($identityOrderId, function($value) { return $value !== null && $value !== false && $value !== "" && $value !== 0; @@ -102,8 +117,14 @@ trait GetOrderIdTrait $memberService = new MemberService(); $userId = $memberService->getUserIdByMobile($filter['promoter_mobile'], $filter['company_id']); if ($userId) { - $sql = "select brokerage.order_id from popularize_promoter promoter left join popularize_brokerage brokerage on promoter.user_id=brokerage.user_id where promoter.company_id=".$filter['company_id']." and promoter.user_id=".$userId; - $lists = $conn->executeQuery($sql)->fetchAll(); + $qb = $conn->createQueryBuilder(); + $lists = $qb->select('brokerage.order_id') + ->from('popularize_promoter', 'promoter') + ->leftJoin('promoter', 'popularize_brokerage', 'brokerage', 'promoter.user_id = brokerage.user_id') + ->where($qb->expr()->eq('promoter.company_id', $qb->expr()->literal($filter['company_id']))) + ->andWhere($qb->expr()->eq('promoter.user_id', $qb->expr()->literal($userId))) + ->execute() + ->fetchAll(); $promoterOrderId = array_column($lists, 'order_id'); $promoterOrderId = array_filter($promoterOrderId, function($value) { return $value !== null && $value !== false && $value !== "" && $value !== 0; @@ -151,8 +172,18 @@ trait GetOrderIdTrait // p_promoter_mobile:上级推广员手机号 // promoter_is_close:是否结算 $orderIds = array_column($orderLists, 'order_id'); - $sql = "select order_id,user_id,is_close from popularize_brokerage where brokerage_type='first_level' and source='order' and order_id in (".implode($orderIds, ',').")"; - $userLists = $conn->executeQuery($sql)->fetchAll(); + $literalQb = $conn->createQueryBuilder(); + $orderIdLiterals = array_map(function ($value) use ($literalQb) { + return $literalQb->expr()->literal($value); + }, $orderIds); + $qb = $conn->createQueryBuilder(); + $userLists = $qb->select('order_id', 'user_id', 'is_close') + ->from('popularize_brokerage') + ->where($qb->expr()->eq('brokerage_type', $qb->expr()->literal('first_level'))) + ->andWhere($qb->expr()->eq('source', $qb->expr()->literal('order'))) + ->andWhere($qb->expr()->in('order_id', $orderIdLiterals)) + ->execute() + ->fetchAll(); $userIds = array_column($userLists, 'user_id'); $userIds = array_filter($userIds, function($value) { return $value !== null && $value !== false && $value !== "" && $value !== 0; @@ -162,8 +193,22 @@ trait GetOrderIdTrait } $result = []; // 查询推广员 - $sql = "select promoter.user_id,promoter.promoter_name,promoter.pname p_promoter_name,promoter.pmobile p_promoter_mobile,identity.name promoter_identity from popularize_promoter promoter left join popularize_promoter_identity identity on promoter.identity_id=identity.id where promoter.user_id in (".implode($userIds, ',').")"; - $promoterLists = $conn->executeQuery($sql)->fetchAll(); + $userIdLiterals = array_map(function ($value) use ($literalQb) { + return $literalQb->expr()->literal($value); + }, $userIds); + $qb = $conn->createQueryBuilder(); + $promoterLists = $qb->select( + 'promoter.user_id', + 'promoter.promoter_name', + 'promoter.pname AS p_promoter_name', + 'promoter.pmobile AS p_promoter_mobile', + 'identity.name AS promoter_identity' + ) + ->from('popularize_promoter', 'promoter') + ->leftJoin('promoter', 'popularize_promoter_identity', 'identity', 'promoter.identity_id = identity.id') + ->where($qb->expr()->in('promoter.user_id', $userIdLiterals)) + ->execute() + ->fetchAll(); $promoterLists = array_column($promoterLists, null, 'user_id'); // 查询推广员手机号 @@ -175,16 +220,35 @@ trait GetOrderIdTrait $promoterData[$value['order_id']]['promoter_is_close'] = $value['is_close']; } // 查询订单分佣金额 - $where = "company_id=".$companyId." and order_id in (".implode($orderIds, ',').") and source='order'"; - $sql = "select order_id,sum(rebate) order_total_rebate from popularize_brokerage where ".$where." group by order_id"; - $orderRebate = $conn->executeQuery($sql)->fetchAll(); + $buildBrokerageRebateQuery = function ($brokerageType = null) use ($conn, $companyId, $orderIdLiterals) { + $qb = $conn->createQueryBuilder(); + $qb->from('popularize_brokerage') + ->where($qb->expr()->eq('company_id', $qb->expr()->literal($companyId))) + ->andWhere($qb->expr()->in('order_id', $orderIdLiterals)) + ->andWhere($qb->expr()->eq('source', $qb->expr()->literal('order'))); + if ($brokerageType !== null) { + $qb->andWhere($qb->expr()->eq('brokerage_type', $qb->expr()->literal($brokerageType))); + } + return $qb; + }; + $orderRebate = $buildBrokerageRebateQuery() + ->select('order_id', 'SUM(rebate) AS order_total_rebate') + ->groupBy('order_id') + ->execute() + ->fetchAll(); $orderRebate = array_column($orderRebate, null, 'order_id'); // 查询订单分佣金额 - $sql = "select order_id,sum(rebate) as rebate from popularize_brokerage where ".$where." and brokerage_type='first_level' group by order_id"; - $firstRebate = $conn->executeQuery($sql)->fetchAll(); + $firstRebate = $buildBrokerageRebateQuery('first_level') + ->select('order_id', 'SUM(rebate) AS rebate') + ->groupBy('order_id') + ->execute() + ->fetchAll(); $firstRebate = array_column($firstRebate, null, 'order_id'); - $sql = "select order_id,sum(rebate) as rebate from popularize_brokerage where ".$where." and brokerage_type='second_level' group by order_id"; - $secondRebate = $conn->executeQuery($sql)->fetchAll(); + $secondRebate = $buildBrokerageRebateQuery('second_level') + ->select('order_id', 'SUM(rebate) AS rebate') + ->groupBy('order_id') + ->execute() + ->fetchAll(); $secondRebate = array_column($secondRebate, null, 'order_id'); foreach ($orderIds as $order_id) { if (isset($promoterData[$order_id])) { diff --git a/src/PromotionsBundle/Traits/CheckPromotionsValid.php b/src/PromotionsBundle/Traits/CheckPromotionsValid.php index 8b2f41e..ba8340b 100644 --- a/src/PromotionsBundle/Traits/CheckPromotionsValid.php +++ b/src/PromotionsBundle/Traits/CheckPromotionsValid.php @@ -246,7 +246,7 @@ trait CheckPromotionsValid * @param inteter $itemId 商品详情也的商品ID,也是默认商品ID * @param bool $isItemsAll 如果商品为多规格商品是否需要查询所有的SKU信息 */ - public function checkCurrentMemberpreferenceByItemId($companyId, $userId, $itemId, $distributorId = null, $isItemsAll = true, &$msg) + public function checkCurrentMemberpreferenceByItemId($companyId, $userId, $itemId, &$msg, $distributorId = null, $isItemsAll = true) { $itemsService = new ItemsService(); $itemInfo = $itemsService->getInfo(['item_id' => $itemId, 'company_id' => $companyId]); diff --git a/src/ThemeBundle/Http/FrontApi/V1/Action/OpenScreenAd.php b/src/ThemeBundle/Http/FrontApi/V1/Action/OpenScreenAd.php index 83647dd..5af2e0f 100644 --- a/src/ThemeBundle/Http/FrontApi/V1/Action/OpenScreenAd.php +++ b/src/ThemeBundle/Http/FrontApi/V1/Action/OpenScreenAd.php @@ -60,17 +60,28 @@ class OpenScreenAd extends Controller */ public function getInfo(Request $request) { - // CONST: 1E236443 - $params = $request->all('company_id'); $auth_info = $request->get('auth'); $filter['company_id'] = $auth_info['company_id']; $filter['is_enable'] = 1; - $filter['start_time|lte'] = time(); - $filter['end_time|gte'] = time(); $OpenScreenAd = new OpenScreenAdServices(); $data = $OpenScreenAd->lists($filter, '*', 1, 1); $result = !empty($data['list']) ? reset($data['list']) : []; + + // 只有配置了开始/结束时间才校验;都是 0 表示不限期(管理端未传时间时的默认值) + if (!empty($result)) { + $now = time(); + $startTime = (int) ($result['start_time'] ?? 0); + $endTime = (int) ($result['end_time'] ?? 0); + if (!($startTime === 0 && $endTime === 0)) { + if ($startTime > 0 && $startTime > $now) { + $result = []; + } elseif ($endTime > 0 && $endTime < $now) { + $result = []; + } + } + } + if ($result) { $result['ad_url'] = json_decode($result['ad_url'], true); } diff --git a/src/ThemeBundle/Http/FrontApi/V1/Action/PagesTemplate.php b/src/ThemeBundle/Http/FrontApi/V1/Action/PagesTemplate.php index a032b10..41f225d 100644 --- a/src/ThemeBundle/Http/FrontApi/V1/Action/PagesTemplate.php +++ b/src/ThemeBundle/Http/FrontApi/V1/Action/PagesTemplate.php @@ -122,12 +122,6 @@ class PagesTemplate extends Controller if ($result['data']) { $result['data'] = $itemsService->applyMultiSpecTotalStoreForItemList($result['data']); } - if ($eActivityId > 0 && $result['data']) { - $activityItemsService = new ActivityItemsService(); - $wrapped = ['list' => $result['data']]; - $wrapped = $activityItemsService->getItemsListActityPrice($wrapped, $eActivityId, (int) $params['company_id']); - $result['data'] = $wrapped['list']; - } if ($result['data']) { $result['data'] = $itemsService->getItemsListMemberPrice($result['data'], $authInfo['user_id'], $params['company_id']); } @@ -139,6 +133,13 @@ class PagesTemplate extends Controller $promotionType = ''; } $result['data'] = $itemsService->getItemsListActityTag($result['data'], $params['company_id'], $params['regionauth_id'], $params['user_id'], $promotionId, $promotionType, 'include_not_start'); + // 内购活动价最后覆盖,避免被营销标签 activity_price 覆盖 + if ($eActivityId > 0 && $result['data']) { + $activityItemsService = new ActivityItemsService(); + $wrapped = ['list' => $result['data']]; + $wrapped = $activityItemsService->getItemsListActityPrice($wrapped, $eActivityId, (int) $params['company_id']); + $result['data'] = $wrapped['list']; + } } //优惠券标签 diff --git a/src/ThemeBundle/Services/OpenScreenAdServices.php b/src/ThemeBundle/Services/OpenScreenAdServices.php index 000704a..bd009a2 100644 --- a/src/ThemeBundle/Services/OpenScreenAdServices.php +++ b/src/ThemeBundle/Services/OpenScreenAdServices.php @@ -73,8 +73,8 @@ class OpenScreenAdServices $saveAdd['waiting_time'] = $params['waiting_time']; $saveAdd['ad_url'] = $params['ad_url']; $saveAdd['app'] = $params['app']; - $saveAdd['start_time'] = $params['start_time']; - $saveAdd['end_time'] = $params['end_time']; + $saveAdd['start_time'] = ($params['start_time'] !== null && $params['start_time'] !== '') ? (int) $params['start_time'] : 0; + $saveAdd['end_time'] = ($params['end_time'] !== null && $params['end_time'] !== '') ? (int) $params['end_time'] : 0; return $this->saveAdd($saveAdd); } else { $saveUpdate['ad_material'] = $params['ad_material']; @@ -86,8 +86,13 @@ class OpenScreenAdServices $saveUpdate['waiting_time'] = $params['waiting_time']; $saveUpdate['ad_url'] = $params['ad_url']; $saveUpdate['app'] = $params['app']; - $saveUpdate['start_time'] = $params['start_time']; - $saveUpdate['end_time'] = $params['end_time']; + // 没传 start_time / end_time 时不更新,避免每次保存都写成 0 + if ($params['start_time'] !== null && $params['start_time'] !== '') { + $saveUpdate['start_time'] = (int) $params['start_time']; + } + if ($params['end_time'] !== null && $params['end_time'] !== '') { + $saveUpdate['end_time'] = (int) $params['end_time']; + } $saveUpdate['updated'] = time(); return $this->saveUpdate($company_id, $saveUpdate); diff --git a/src/ThemeBundle/Services/PagesTemplateServices.php b/src/ThemeBundle/Services/PagesTemplateServices.php index abaec07..396f157 100644 --- a/src/ThemeBundle/Services/PagesTemplateServices.php +++ b/src/ThemeBundle/Services/PagesTemplateServices.php @@ -1401,7 +1401,7 @@ class PagesTemplateServices if (!$distributor_ids) { break; } - $distributor_list = $distributorService->entityRepository->getLists(['distributor_id' => $distributor_ids], 'distributor_id, name, logo, first_letter, tag_name, tag_start_time, tag_end_time'); + $distributor_list = $distributorService->entityRepository->getLists(['distributor_id' => $distributor_ids], 'distributor_id, name, logo, first_letter'); if (!$distributor_list) { break; } @@ -1421,10 +1421,6 @@ class PagesTemplateServices if (!$distributor_info) { continue; } - //店铺标签不在有效期内 - if (intval($distributor_info['tag_start_time']) > time() or intval($distributor_info['tag_end_time']) < time()) { - $distributor_info['tag_name'] = ''; - } $child_data_v = array_merge($child_data_v, $distributor_info); $child_v['data'][$child_data_k] = $child_data_v; } @@ -1445,7 +1441,7 @@ class PagesTemplateServices if (!$distributor_ids) { break; } - $distributor_list = $distributorService->entityRepository->getLists(['distributor_id' => $distributor_ids], 'distributor_id, name, logo, first_letter, tag_name, tag_start_time, tag_end_time'); + $distributor_list = $distributorService->entityRepository->getLists(['distributor_id' => $distributor_ids], 'distributor_id, name, logo, first_letter'); if (!$distributor_list) { break; } @@ -1456,10 +1452,6 @@ class PagesTemplateServices if (!$distributor_info) { continue; } - //店铺标签不在有效期内 - if (intval($distributor_info['tag_start_time']) > time() or intval($distributor_info['tag_end_time']) < time()) { - $distributor_info['tag_name'] = ''; - } $tmp_v = array_merge($tmp_v, $distributor_info); $params['data'][$tmp_k] = $tmp_v; } diff --git a/src/WechatBundle/Http/Api/V1/Action/Wxa.php b/src/WechatBundle/Http/Api/V1/Action/Wxa.php index a071eed..cb24792 100644 --- a/src/WechatBundle/Http/Api/V1/Action/Wxa.php +++ b/src/WechatBundle/Http/Api/V1/Action/Wxa.php @@ -761,7 +761,7 @@ class Wxa extends Controller $list = $settingService->getTemplateConf($companyId, $templateName, $pageName, $name, $version); - if (!isset($list[0]['params']['is_open'])) { + if (!empty($list) && !isset($list[0]['params']['is_open'])) { $list[0]['params']['is_open'] = true; } @@ -1265,5 +1265,4 @@ class Wxa extends Controller return $this->response->array($response); } - } diff --git a/tests/GoodsBatchItemsActionTest.php b/tests/GoodsBatchItemsActionTest.php new file mode 100644 index 0000000..a7880b4 --- /dev/null +++ b/tests/GoodsBatchItemsActionTest.php @@ -0,0 +1,83 @@ + 1, + 'item_name' => '商品A', + 'price' => 1000, + 'pics' => ['/a.jpg'], + ], + [ + 'item_id' => 2, + 'item_name' => '商品B', + 'price' => 2000, + 'pics' => ['/b.jpg'], + ], + ]; + + $repository = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getLists']) + ->getMock(); + $repository->expects($this->once()) + ->method('getLists') + ->with(['item_id' => [1, 2, 3]], 'item_id,item_name,price,pics', 1, -1, []) + ->willReturn($expected); + + $this->bindItemsRepository($repository); + + $request = Request::create('/h5app/wxapp/goods/items/batch', 'GET', [ + 'item_ids' => '1,2,3', + ]); + $request->attributes->set('auth', ['company_id' => 1]); + + $response = (new Items())->getBatchItems($request); + + $this->assertSame($expected, $response->getOriginalContent()); + } + + public function testGetBatchItemsReturnsEmptyArrayWhenItemIdsEmpty(): void + { + $repository = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getLists']) + ->getMock(); + $repository->expects($this->never())->method('getLists'); + + $this->bindItemsRepository($repository); + + $request = Request::create('/h5app/wxapp/goods/items/batch', 'GET', [ + 'item_ids' => '', + ]); + $request->attributes->set('auth', ['company_id' => 1]); + + $response = (new Items())->getBatchItems($request); + + $this->assertSame([], $response->getOriginalContent()); + } + + private function bindItemsRepository($repository): void + { + $manager = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getRepository']) + ->getMock(); + $manager->method('getRepository') + ->with(ItemsEntity::class) + ->willReturn($repository); + + $registry = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getManager']) + ->getMock(); + $registry->method('getManager') + ->with('default') + ->willReturn($manager); + + $this->app->instance('registry', $registry); + } +} diff --git a/tests/StoreHomePageServiceInternalNameTest.php b/tests/StoreHomePageServiceInternalNameTest.php index 38dfdde..f33e8b5 100644 --- a/tests/StoreHomePageServiceInternalNameTest.php +++ b/tests/StoreHomePageServiceInternalNameTest.php @@ -43,4 +43,106 @@ final class StoreHomePageServiceInternalNameTest extends TestCase $this->assertFalse(StoreHomePageService::pageTemplateDetailHasNonEmptyList(['list' => []])); $this->assertTrue(StoreHomePageService::pageTemplateDetailHasNonEmptyList(['list' => [['x' => 1]]])); } + + public function testCustomDecorationSettingVersionCandidatesForShopIncludesV101Fallback(): void + { + $this->assertSame( + ['shop_12', 'v1.0.1'], + StoreHomePageService::customDecorationSettingVersionCandidates(12, 0) + ); + } + + public function testCustomDecorationSettingVersionCandidatesForHeadquarters(): void + { + $this->assertSame( + ['v1.0.1'], + StoreHomePageService::customDecorationSettingVersionCandidates(0, 0) + ); + } + + public function testDecorationTemplateNameCandidatesFallsBackToYykweishop(): void + { + $this->assertSame( + ['my_theme', 'yykweishop'], + StoreHomePageService::decorationTemplateNameCandidates(['template_name' => 'my_theme']) + ); + } + + public function testPagesTemplateListSearchPlansForShop(): void + { + $this->assertSame( + [ + ['distributor_id' => 8, 'weapp_pages' => 'distributor_index'], + ['distributor_id' => 8, 'weapp_pages' => 'index'], + ['distributor_id' => 0, 'weapp_pages' => 'index'], + ], + StoreHomePageService::pagesTemplateListSearchPlans(8) + ); + } + + public function testBuildPageTemplateDetailFromTemplateConfList(): void + { + $detail = StoreHomePageService::buildPageTemplateDetailFromTemplateConfList([ + ['name' => 'slider', 'params' => ['name' => 'slider', 'base' => [], 'data' => []]], + ['name' => 'plain', 'params' => ['title' => 'x']], + ]); + $this->assertCount(1, $detail['config']); + $this->assertCount(2, $detail['list']); + } + + public function testSafeDecodeWeappSettingParamsSupportsSerializeAndJson(): void + { + $payload = ['name' => 'page', 'base' => []]; + $this->assertSame($payload, StoreHomePageService::safeDecodeWeappSettingParams(serialize($payload))); + $this->assertSame($payload, StoreHomePageService::safeDecodeWeappSettingParams(json_encode($payload))); + } + + public function testBuildTemplateConfListFromWeappSettingEntities(): void + { + $payload = ['name' => 'page', 'base' => []]; + $entity = new class($payload) { + private $params; + + public function __construct(array $params) + { + $this->params = serialize($params); + } + + public function getId() + { + return 53453; + } + + public function getTemplateName() + { + return 'yykweishop'; + } + + public function getCompanyId() + { + return 34; + } + + public function getName() + { + return 'page'; + } + + public function getPageName() + { + return 'custom_106'; + } + + public function getParams() + { + return $this->params; + } + }; + + $list = StoreHomePageService::buildTemplateConfListFromWeappSettingEntities([$entity], 34, 'yykweishop'); + $this->assertCount(1, $list); + $this->assertSame('page', $list[0]['name']); + $this->assertSame($payload, $list[0]['params']); + } + }