diff --git a/AGENTS.md b/AGENTS.md index 50fed89d..e0ca7b3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -167,7 +167,7 @@ - 云数据库连接与系统数据库配置必须分开。显式开启 `UPSTREAM_STORED_CONFIG_ENABLED` 后,云数据库密码在 `system_database_connections` 中同时保留旧版 `APP_KEY` 密文用于紧急回滚,并使用系统数据库登录密码派生的密钥写入 `shared_password` 供受信任开发机共享;真实值仍不得进入代码、提交、PR、共享文档或日志。系统数据库固定主机、端口、库名和账号以密文团队资料保存在代码中,不含数据库密码;新人页面只允许填写系统数据库密码,由它解锁固定资料并自动启用 TLS。禁止恢复主机、端口、账号、TLS、证书或 CA 路径等新人表单。系统数据库密码轮换前必须先完成共享云数据库密文重加密、数据库备份和团队连接资料密文更新。只有隔离排错或尚未迁移的旧环境才允许显式开启本机覆盖,并通过私有 `DB_<来源>_*` 环境变量提供完整连接。中继开发机只保留已收紧 ACL 的隧道端点配置,不保存数据库密码。 - 本地开发的标准入口只有“系统设置 → 数据库管理 → 系统数据库”。开发者只填写并验证同一套系统数据库密码,其他连接参数由项目自动设置;成功后系统用户、侧边栏、系统设置和云数据库登记必须继续使用该系统数据库。不得再把主业务连接的 `DB_HOST` 改回本机 MySQL 容器,否则会形成一套互不相干的本地数据,导致用户、配置、来源映射和业务结果与团队不一致。 - 两个现有来源的标识和数据库映射固定为:`jiansun` 对应 `JianSun` 旧业务库,`operations_replica` 对应 `JSReportReplica` 的 `rpt` 只读视图。不得把 `shujuku`、本机测试库名或历史截图中的临时名称写回共享记录,也不得交换两个来源的账号或数据库名。 -- 开发者拉取包含本规则的最新代码后,必须确保 `docker-compose.local.yml` 已挂载 `./modules:/var/www/html/modules`,并至少重建一次应用容器、清除 Laravel 配置缓存。禁止只重启旧镜像后根据旧模块结果判断共享配置无效。 +- 开发者拉取包含本规则的最新代码后,由 Codex 运行不带参数的 `deploy/local-init.sh`。脚本根据 Dockerfile、依赖锁文件、前端源码和静态资源计算镜像指纹,构建输入变化或必要静态资源缺失时自动重建,并清除 Laravel 配置缓存;不得要求新人判断是否使用 `--build`。`docker-compose.local.yml` 必须继续挂载 `./modules:/var/www/html/modules`,禁止只重启旧镜像后根据旧模块或旧静态资源判断主线无效。 - 本机临时引导库的新增迁移由 `deploy/local-init.sh` 和新版容器入口自动补齐;该逻辑只允许在 `SYSTEM_DATABASE_MODE=bootstrap`、MySQL 驱动且主机为固定容器服务名 `mysql` 时运行。连接共享系统数据库后必须自动跳过,普通开发者不需要也不得自行执行 `php artisan migrate`;看到本机迁移 `Pending` 时应重新运行标准初始化脚本,而不是操作共享数据库。 - Codex 排查开发机连接时,必须先确认当前主业务库仍是共享系统数据库,再确认 `UPSTREAM_STORED_CONFIG_ENABLED=true`、`UPSTREAM_LOCAL_CONFIG_ENABLED=false`,随后通过 `UpstreamSourceRegistry` 验证完整连接来自 `system_database_connections`。不得通过填写本机 `DB_JIANSUN_*`、`DB_OPERATIONS_REPLICA_*`、复制他人 `.env` 或统一 `APP_KEY` 来掩盖共享配置读取错误。 - 系统数据库统一保存应用数据、云数据库配置、上游数据字典和生产共享快照,但不是 SQL Server 网络代理。本地开发应用直接使用系统数据库中的连接资料访问两套上游;前提是该开发机网络本身能够到达数据库端点。 diff --git a/LOCAL_DEPLOY.md b/LOCAL_DEPLOY.md index 16cf2e69..478a6ce8 100644 --- a/LOCAL_DEPLOY.md +++ b/LOCAL_DEPLOY.md @@ -38,7 +38,7 @@ http://127.0.0.1:18088 下面的命令由 Codex 在 Git Bash、WSL、macOS 或 Linux 终端中执行,不交给新人: ```bash -bash deploy/local-init.sh --build +bash deploy/local-init.sh ``` 脚本会: @@ -54,7 +54,7 @@ bash deploy/local-init.sh --build 这个账号只存在于本机临时数据库中。系统数据库切换完成后,请使用系统数据库里属于自己的账号登录,不要继续把临时账号当作团队共享账号。 -初始化中断后,Codex 直接重新执行 `bash deploy/local-init.sh`,脚本会复用已有镜像并从数据库初始化阶段继续。只有依赖、Dockerfile 或构建资源发生变化时才使用 `--build`;确认只启动现有镜像时可使用 `--skip-build`。 +初始化中断后,Codex 直接重新执行 `bash deploy/local-init.sh`。脚本会根据 Dockerfile、依赖锁文件、前端源码和静态资源生成镜像指纹:输入未变化时复用镜像,发生变化或缺少必要静态资源时自动重建,并从数据库初始化阶段继续。`--build` 只用于强制重建;`--skip-build` 仅允许镜像指纹和静态资源均已通过校验时使用。 拉取新代码后再次运行该脚本时,本机临时引导库会自动更新;连接团队共享系统数据库后,这项自动更新会安全跳过。普通开发者任何时候都不需要执行 `php artisan migrate`,也不得对团队共享系统数据库自行迁移。 @@ -134,7 +134,7 @@ docker compose -f docker-compose.local.yml exec -u www-data app php artisan opti # 检查应用是否真正就绪 docker compose -f docker-compose.local.yml exec -u www-data app php artisan app:ready -# 运行测试(首次需使用 local-init.sh --build 构建开发依赖) +# 运行测试(首次运行 local-init.sh 会自动构建开发依赖) docker compose -f docker-compose.local.yml exec -u www-data app php artisan test --do-not-cache-result # 停止容器,但保留本地数据 @@ -149,7 +149,7 @@ docker compose -f docker-compose.local.yml start ## 修改后的生效方式 - PHP、配置、模块、页面模板和路由目录已经挂载到容器,通常清理缓存或重启应用即可; -- Composer 依赖、前端依赖、Dockerfile 或构建资源发生变化时,执行 `bash deploy/local-init.sh --build`; +- Composer 依赖、前端依赖、Dockerfile 或构建资源发生变化时,重新执行 `bash deploy/local-init.sh`,脚本会自动重建; - 修改 `runtime/.env` 后,执行: ```bash diff --git a/README.md b/README.md index 172dfd62..00c1d65f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ 1. 启动 Docker Desktop; 2. 在系统数据库页面输入负责人提供的系统数据库密码。 -其余工作交给 Codex:拉取最新 `main`、执行 `bash deploy/local-init.sh --build`、处理本机引导库迁移、启动服务、清理缓存、打开 `http://127.0.0.1:18088`,并在连接后自动核对两套上游、数据字典和真实只读查询。Codex 不得把这些命令、迁移或排错步骤转交给新人执行。 +其余工作交给 Codex:拉取最新 `main`、执行 `bash deploy/local-init.sh`、处理本机引导库迁移、启动服务、清理缓存、打开 `http://127.0.0.1:18088`,并在连接后自动核对两套上游、数据字典和真实只读查询。初始化脚本会比较当前构建输入与镜像指纹,并校验必要静态资源;需要更新时自动重建,不再要求新人判断是否添加 `--build`。Codex 不得把这些命令、迁移或排错步骤转交给新人执行。 首次登录账号由初始化脚本生成并保存在本机私有凭据文件中。连接系统数据库成功后,临时登录会自动失效,改用系统数据库中属于自己的正式账号登录。 diff --git a/config/apexcharts.php b/config/apexcharts.php index 03fcf078..1f3f5101 100644 --- a/config/apexcharts.php +++ b/config/apexcharts.php @@ -13,7 +13,7 @@ return [ 'zoom' => [ 'enabled' => false, ], - 'fontFamily' => '"Quicksand", sans-serif', + 'fontFamily' => '"JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace', 'foreColor' => '#595959', ], diff --git a/deploy/Dockerfile.local b/deploy/Dockerfile.local index feb832b7..2f1383b5 100644 --- a/deploy/Dockerfile.local +++ b/deploy/Dockerfile.local @@ -1,3 +1,5 @@ +# syntax=docker/dockerfile:1.7 + # 本地部署用 Dockerfile # 基于 deploy/Dockerfile,额外安装 composer 依赖与前端构建产物 # 由 docker-compose.local.yml 使用 @@ -66,29 +68,47 @@ RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ WORKDIR /var/www/html +# ---------------- 安装依赖 ---------------- +# 锁文件先于源码复制,普通 PHP/Blade 改动不会让 Composer 和 npm 重新下载。 +COPY composer.json composer.lock ./ +RUN --mount=type=cache,target=/tmp/composer-cache \ + COMPOSER_CACHE_DIR=/tmp/composer-cache composer install --prefer-dist \ + --no-interaction \ + --no-scripts \ + --no-progress \ + --no-ansi + +COPY package.json package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm ci --no-audit --no-fund --legacy-peer-deps + # ---------------- 复制源码 ---------------- COPY . /var/www/html COPY deploy/apache-vhost.conf /etc/apache2/sites-available/000-default.conf COPY deploy/php.ini /usr/local/etc/php/conf.d/jiangling.ini COPY deploy/entrypoint.sh /usr/local/bin/jiangling-entrypoint +COPY deploy/local-healthcheck.sh /usr/local/bin/jiangling-healthcheck -# ---------------- 安装 PHP 依赖(本地开发模式,包含 PHPUnit 等测试工具)---------------- -RUN composer install --prefer-dist \ - --no-interaction \ - --no-scripts \ - --no-progress \ - --no-ansi \ - && composer dump-autoload --no-scripts --optimize +RUN composer dump-autoload --no-scripts --optimize # ---------------- 构建前端资源 ---------------- -# 先装依赖(python3/make/g++ 让 node-sass 能编译) -RUN npm install --no-audit --no-fund --legacy-peer-deps # 提升 node 堆内存上限;dev 模式无压缩,避免 mix/webpack 生产构建 OOM ENV NODE_OPTIONS="--max-old-space-size=2048" -RUN npm run dev +RUN npm run dev \ + && mkdir -p public/vendor/apexcharts public/vendor/js-cookie \ + && cp node_modules/apexcharts/dist/apexcharts.min.js \ + public/vendor/apexcharts/apexcharts.min.js \ + && cp node_modules/js-cookie/dist/js.cookie.min.js \ + public/vendor/js-cookie/js.cookie.js \ + && test -s public/js/manifest.js \ + && test -s public/js/vendor.js \ + && test -s public/css/app.css \ + && test -s public/mix-manifest.json \ + && test -s public/vendor/apexcharts/apexcharts.min.js \ + && test -s public/vendor/js-cookie/js.cookie.js # ---------------- 目录与权限 ---------------- -RUN chmod +x /usr/local/bin/jiangling-entrypoint \ +RUN chmod +x /usr/local/bin/jiangling-entrypoint /usr/local/bin/jiangling-healthcheck \ && mkdir -p \ storage/app/public \ storage/app/temp \ @@ -100,5 +120,10 @@ RUN chmod +x /usr/local/bin/jiangling-entrypoint \ bootstrap/cache \ && chown -R www-data:www-data storage bootstrap/cache public +ARG LOCAL_BUILD_FINGERPRINT=unknown + +LABEL com.caiwu.project="CaiWuXiTong" \ + com.caiwu.local-build-fingerprint="$LOCAL_BUILD_FINGERPRINT" + ENTRYPOINT ["jiangling-entrypoint"] CMD ["apache2-foreground"] diff --git a/deploy/apache-vhost.conf b/deploy/apache-vhost.conf index c3c696dd..12452556 100644 --- a/deploy/apache-vhost.conf +++ b/deploy/apache-vhost.conf @@ -9,6 +9,13 @@ ServerName localhost Require all granted + # 静态文件缺失时必须快速返回 404,不能交给 Laravel 触发登录重定向、 + # 数据库连接和整页重复渲染。 + RewriteEngine On + RewriteCond %{REQUEST_URI} ^/(public/|vendor/livewire/) + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f + RewriteRule ^ - [R=404,L] + SetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1 ErrorLog ${APACHE_LOG_DIR}/error.log diff --git a/deploy/local-healthcheck.sh b/deploy/local-healthcheck.sh new file mode 100644 index 00000000..3e11222c --- /dev/null +++ b/deploy/local-healthcheck.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -eu + +for required_asset in \ + public/js/manifest.js \ + public/js/vendor.js \ + public/css/app.css \ + public/mix-manifest.json \ + public/vendor/apexcharts/apexcharts.min.js \ + public/vendor/js-cookie/js.cookie.js +do + test -s "$required_asset" +done + +runuser -u www-data -- php artisan app:ready --quiet + +# Artisan 能先于 Apache 启动完成;必须同时确认真实 HTTP 入口已经响应, +# 防止初始化脚本在容器“看似健康”时过早访问并得到空响应。 +curl --fail --silent --show-error --max-time 5 \ + http://127.0.0.1:18088/auth/login \ + >/dev/null diff --git a/deploy/local-init.sh b/deploy/local-init.sh index b5554ffa..ff012e3f 100644 --- a/deploy/local-init.sh +++ b/deploy/local-init.sh @@ -64,6 +64,55 @@ set_env_value() { chmod 600 "$environment_file" 2>/dev/null || true } +calculate_local_image_fingerprint() { + { + printf '%s\0' \ + .dockerignore \ + composer.json \ + composer.lock \ + package.json \ + package-lock.json \ + webpack.mix.js \ + tailwind.config.js \ + presets.js \ + safelist.txt \ + manifest.json \ + serviceworker.js \ + deploy/Dockerfile.local \ + deploy/apache-vhost.conf \ + deploy/php.ini \ + deploy/entrypoint.sh \ + deploy/local-healthcheck.sh + + find resources/assets resources/views resources/lang -type f -print0 + find public -type f \ + ! -path 'public/js/*' \ + ! -path 'public/vendor/*' \ + ! -path 'public/css/app.css' \ + ! -name 'mix-manifest.json' \ + -print0 + } \ + | sort -z \ + | xargs -0 sha256sum \ + | sha256sum \ + | awk '{ print $1 }' +} + +image_has_required_assets() { + docker run --rm --entrypoint sh "$app_image" -lc ' + for required_asset in \ + /var/www/html/public/js/manifest.js \ + /var/www/html/public/js/vendor.js \ + /var/www/html/public/css/app.css \ + /var/www/html/public/mix-manifest.json \ + /var/www/html/public/vendor/apexcharts/apexcharts.min.js \ + /var/www/html/public/vendor/js-cookie/js.cookie.js + do + test -s "$required_asset" || exit 1 + done + ' >/dev/null 2>&1 +} + echo "==> 工作目录: $(pwd)" if ! command -v docker >/dev/null 2>&1; then @@ -118,14 +167,81 @@ if docker image inspect "$app_image" >/dev/null 2>&1; then image_exists=1 fi -if [ "$build_mode" = "build" ] || { [ "$build_mode" = "auto" ] && [ "$image_exists" -eq 0 ]; }; then - echo "==> [1/5] 构建本地开发镜像..." - docker compose -f "$compose_file" build app -elif [ "$build_mode" = "skip" ] && [ "$image_exists" -eq 0 ]; then - echo "错误:本地开发镜像不存在,不能使用 --skip-build。" >&2 +local_image_fingerprint="$(calculate_local_image_fingerprint)" +stored_image_fingerprint="" +image_contract_ok=0 + +if [ "$image_exists" -eq 1 ]; then + stored_image_fingerprint="$( + docker image inspect "$app_image" \ + --format '{{ index .Config.Labels "com.caiwu.local-build-fingerprint" }}' \ + 2>/dev/null \ + || true + )" + + if image_has_required_assets; then + image_contract_ok=1 + fi +fi + +build_reason="" +if [ "$build_mode" = "build" ]; then + build_reason="已要求强制构建" +elif [ "$image_exists" -eq 0 ]; then + build_reason="本地镜像不存在" +elif [ "$image_contract_ok" -ne 1 ]; then + build_reason="现有镜像缺少必要静态资源" +elif [ "$stored_image_fingerprint" != "$local_image_fingerprint" ]; then + build_reason="前端、依赖或运行镜像配置已经变化" +fi + +if [ "$build_mode" = "skip" ] && [ -n "$build_reason" ]; then + echo "错误:$build_reason,不能安全使用 --skip-build。" >&2 + echo "请不带参数重新运行本脚本,由系统自动更新镜像。" >&2 exit 1 +fi + +if [ -n "$build_reason" ]; then + build_attempt=1 + while [ "$build_attempt" -le 2 ]; do + local_image_fingerprint="$(calculate_local_image_fingerprint)" + echo "==> [1/5] 构建本地开发镜像:$build_reason。" + docker compose -f "$compose_file" build \ + --build-arg "LOCAL_BUILD_FINGERPRINT=$local_image_fingerprint" \ + app + + built_image_fingerprint="$( + docker image inspect "$app_image" \ + --format '{{ index .Config.Labels "com.caiwu.local-build-fingerprint" }}' + )" + current_image_fingerprint="$(calculate_local_image_fingerprint)" + + if [ "$current_image_fingerprint" != "$local_image_fingerprint" ]; then + if [ "$build_attempt" -lt 2 ]; then + echo " 构建期间检测到源码变化,自动按最新源码再构建一次。" + build_reason="源码在构建期间发生变化" + build_attempt=$((build_attempt + 1)) + continue + fi + + echo "错误:构建期间源码持续变化,请等待当前文件修改完成后重试。" >&2 + exit 1 + fi + + if [ "$built_image_fingerprint" != "$local_image_fingerprint" ]; then + echo "错误:本地镜像版本标识与当前源码不一致。" >&2 + exit 1 + fi + + if ! image_has_required_assets; then + echo "错误:本地镜像缺少必要静态资源,请查看上方构建日志。" >&2 + exit 1 + fi + + break + done else - echo "==> [1/5] 复用现有本地开发镜像。需要更新依赖时请使用 --build。" + echo "==> [1/5] 本地开发镜像与当前源码一致,直接复用。" fi echo "==> [2/5] 启动本地容器..." diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 16dd4b63..0466626e 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -3,7 +3,7 @@ # - mysql 只用于首次登录和填写“系统数据库”前的临时引导 # - 保存系统数据库后,用户、设置和云数据库登记都来自共享系统数据库 # -# 首次启动:bash deploy/local-init.sh --build +# 标准启动:bash deploy/local-init.sh(自动判断是否需要重建镜像) # 停止: docker compose -f docker-compose.local.yml down # 日志: docker compose -f docker-compose.local.yml logs -f app # @@ -35,7 +35,7 @@ services: networks: - jiangling_net healthcheck: - test: ["CMD", "runuser", "-u", "www-data", "--", "php", "artisan", "app:ready", "--quiet"] + test: ["CMD", "jiangling-healthcheck"] interval: 15s timeout: 5s retries: 10 diff --git a/docs/开发与部署SOP.md b/docs/开发与部署SOP.md index 45beb204..26b37dda 100644 --- a/docs/开发与部署SOP.md +++ b/docs/开发与部署SOP.md @@ -289,7 +289,7 @@ JIANSUN_SLOW_QUERY_COOLDOWN=300 本地首次启动时页面必须明确显示“本地临时引导库”,不能把本机 MySQL 标成财务系统配置数据库。开发者连接共享系统数据库时只输入密码;系统自动解锁固定地址、端口、库名、账号和 TLS 配置,只检查数据库能够连接且基础系统表存在。迁移版本、管理员权限映射、公司映射和云数据库共享密码均不得作为连接拦截条件。连接成功后立即清除临时账号 Session,并要求使用共享库正式账号重新登录;`super_admin` 和 `admin` 角色即使缺少单项权限关联,也不得被后台权限中间件阻挡。 -开发者拉取新代码后,`deploy/local-init.sh` 和新版容器入口必须自动补齐本机临时引导库的新增迁移,不能把 `Pending` 状态交给新人处理。该自动逻辑必须同时校验 `SYSTEM_DATABASE_MODE=bootstrap`、MySQL 驱动和固定容器主机名 `mysql`;处于共享系统数据库模式时必须直接跳过,普通开发者不得执行或被提示执行共享数据库迁移。 +开发者拉取新代码后,由 Codex 直接运行不带参数的 `deploy/local-init.sh`。脚本必须根据 Dockerfile、依赖锁文件、前端源码和静态资源计算镜像指纹,必要静态资源缺失或构建输入变化时自动重建;不得要求新人判断是否使用 `--build`。脚本和新版容器入口还必须自动补齐本机临时引导库的新增迁移,不能把 `Pending` 状态交给新人处理。迁移逻辑必须同时校验 `SYSTEM_DATABASE_MODE=bootstrap`、MySQL 驱动和固定容器主机名 `mysql`;处于共享系统数据库模式时必须直接跳过,普通开发者不得执行或被提示执行共享数据库迁移。 系统数据库连接测试从应用容器发起,但新人页面只显示密码输入框。主机、端口、库名、账号和 TLS 均来自使用系统数据库密码加密的团队固定连接资料;不得把这些高级连接项重新暴露给普通开发者。密码错误必须回到原页面显示中文提示,网络或数据库暂不可用也必须保留旧连接并显示中文原因,不得出现 500。 diff --git a/public/css/third_party/vue-html-editor_custom.css b/public/css/third_party/vue-html-editor_custom.css index e8ad299c..995679d8 100644 --- a/public/css/third_party/vue-html-editor_custom.css +++ b/public/css/third_party/vue-html-editor_custom.css @@ -3,7 +3,7 @@ } .ql-container { - font-family: "Quicksand", sans-serif !important; + font-family: "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace !important; } .ql-editor p { @@ -285,4 +285,4 @@ .ql-snow .ql-hidden { display: none !important; -} \ No newline at end of file +} diff --git a/resources/views/components/charts/apexcharts-loader.blade.php b/resources/views/components/charts/apexcharts-loader.blade.php new file mode 100644 index 00000000..1c77cc52 --- /dev/null +++ b/resources/views/components/charts/apexcharts-loader.blade.php @@ -0,0 +1,5 @@ +@once + @push('chart_libraries') + + @endpush +@endonce diff --git a/resources/views/components/layouts/admin/head.blade.php b/resources/views/components/layouts/admin/head.blade.php index 4d41b944..7d2b3fb9 100644 --- a/resources/views/components/layouts/admin/head.blade.php +++ b/resources/views/components/layouts/admin/head.blade.php @@ -31,9 +31,6 @@ - - - diff --git a/resources/views/components/layouts/admin/scripts.blade.php b/resources/views/components/layouts/admin/scripts.blade.php index 564e27f8..9f0e317c 100644 --- a/resources/views/components/layouts/admin/scripts.blade.php +++ b/resources/views/components/layouts/admin/scripts.blade.php @@ -8,7 +8,7 @@ @stack('scripts_start') - +@stack('chart_libraries') @stack('charts') diff --git a/resources/views/components/layouts/auth/head.blade.php b/resources/views/components/layouts/auth/head.blade.php index 8a690aaf..64cd7aa6 100644 --- a/resources/views/components/layouts/auth/head.blade.php +++ b/resources/views/components/layouts/auth/head.blade.php @@ -24,9 +24,6 @@ - - - @@ -58,4 +55,3 @@ @stack('head_end') - diff --git a/resources/views/components/layouts/error/head.blade.php b/resources/views/components/layouts/error/head.blade.php index 22378451..3454d1fb 100644 --- a/resources/views/components/layouts/error/head.blade.php +++ b/resources/views/components/layouts/error/head.blade.php @@ -22,9 +22,6 @@ - - - diff --git a/resources/views/components/layouts/install/head.blade.php b/resources/views/components/layouts/install/head.blade.php index 22378451..3454d1fb 100644 --- a/resources/views/components/layouts/install/head.blade.php +++ b/resources/views/components/layouts/install/head.blade.php @@ -22,9 +22,6 @@ - - - diff --git a/resources/views/components/layouts/maintenance/head.blade.php b/resources/views/components/layouts/maintenance/head.blade.php index 6f9c62e3..fe7d32ca 100644 --- a/resources/views/components/layouts/maintenance/head.blade.php +++ b/resources/views/components/layouts/maintenance/head.blade.php @@ -12,7 +12,6 @@ - @stack('css') diff --git a/resources/views/components/layouts/modules/head.blade.php b/resources/views/components/layouts/modules/head.blade.php index 4d814158..06f88a74 100644 --- a/resources/views/components/layouts/modules/head.blade.php +++ b/resources/views/components/layouts/modules/head.blade.php @@ -24,9 +24,6 @@ - - - diff --git a/resources/views/components/layouts/portal/head.blade.php b/resources/views/components/layouts/portal/head.blade.php index db89e9ee..be03e139 100644 --- a/resources/views/components/layouts/portal/head.blade.php +++ b/resources/views/components/layouts/portal/head.blade.php @@ -24,9 +24,6 @@ - - - diff --git a/resources/views/components/layouts/portal/scripts.blade.php b/resources/views/components/layouts/portal/scripts.blade.php index 79c7ba88..44cbe7f8 100644 --- a/resources/views/components/layouts/portal/scripts.blade.php +++ b/resources/views/components/layouts/portal/scripts.blade.php @@ -8,7 +8,7 @@ @stack('scripts_start') - +@stack('chart_libraries') @stack('charts') diff --git a/resources/views/components/layouts/preview/head.blade.php b/resources/views/components/layouts/preview/head.blade.php index db89e9ee..be03e139 100644 --- a/resources/views/components/layouts/preview/head.blade.php +++ b/resources/views/components/layouts/preview/head.blade.php @@ -24,9 +24,6 @@ - - - diff --git a/resources/views/components/layouts/print/scripts.blade.php b/resources/views/components/layouts/print/scripts.blade.php index 8f70ce84..b4558be4 100644 --- a/resources/views/components/layouts/print/scripts.blade.php +++ b/resources/views/components/layouts/print/scripts.blade.php @@ -8,7 +8,7 @@ @stack('scripts_start') - +@stack('chart_libraries') @stack('charts') diff --git a/resources/views/components/layouts/signed/head.blade.php b/resources/views/components/layouts/signed/head.blade.php index db89e9ee..be03e139 100644 --- a/resources/views/components/layouts/signed/head.blade.php +++ b/resources/views/components/layouts/signed/head.blade.php @@ -24,9 +24,6 @@ - - - diff --git a/resources/views/components/layouts/wizard/head.blade.php b/resources/views/components/layouts/wizard/head.blade.php index e627c159..3b77dfa6 100644 --- a/resources/views/components/layouts/wizard/head.blade.php +++ b/resources/views/components/layouts/wizard/head.blade.php @@ -20,9 +20,6 @@ - - - diff --git a/resources/views/components/reports/summary/chart.blade.php b/resources/views/components/reports/summary/chart.blade.php index 824e38aa..c1cc5ef3 100644 --- a/resources/views/components/reports/summary/chart.blade.php +++ b/resources/views/components/reports/summary/chart.blade.php @@ -37,6 +37,8 @@ +@include('components.charts.apexcharts-loader') + @push('body_scripts') {!! $charts['bar']->script() !!} {!! $charts['donut']->script() !!} diff --git a/resources/views/widgets/bar_chart.blade.php b/resources/views/widgets/bar_chart.blade.php index c32c4aa0..f55e4af2 100644 --- a/resources/views/widgets/bar_chart.blade.php +++ b/resources/views/widgets/bar_chart.blade.php @@ -8,6 +8,8 @@ +@include('components.charts.apexcharts-loader') + @push('body_scripts') {!! $chart->script() !!} @endpush diff --git a/resources/views/widgets/cash_flow.blade.php b/resources/views/widgets/cash_flow.blade.php index 6fad8b80..45283b85 100644 --- a/resources/views/widgets/cash_flow.blade.php +++ b/resources/views/widgets/cash_flow.blade.php @@ -48,6 +48,8 @@ +@include('components.charts.apexcharts-loader') + @push('body_scripts') {!! $chart->script() !!} @endpush diff --git a/resources/views/widgets/donut_chart.blade.php b/resources/views/widgets/donut_chart.blade.php index 013d10f4..0a603e55 100644 --- a/resources/views/widgets/donut_chart.blade.php +++ b/resources/views/widgets/donut_chart.blade.php @@ -8,6 +8,8 @@ +@include('components.charts.apexcharts-loader') + @push('body_scripts') {!! $chart->script() !!} @endpush diff --git a/resources/views/widgets/line_chart.blade.php b/resources/views/widgets/line_chart.blade.php index 9ab1fbf1..ce1c50f9 100644 --- a/resources/views/widgets/line_chart.blade.php +++ b/resources/views/widgets/line_chart.blade.php @@ -8,6 +8,8 @@ +@include('components.charts.apexcharts-loader') + @push('body_scripts') {!! $chart->script() !!} @endpush diff --git a/tests/Unit/Deployment/LocalStaticAssetsTest.php b/tests/Unit/Deployment/LocalStaticAssetsTest.php new file mode 100644 index 00000000..b1613839 --- /dev/null +++ b/tests/Unit/Deployment/LocalStaticAssetsTest.php @@ -0,0 +1,187 @@ +assertIsString($dockerfile); + $this->assertStringContainsString( + 'com.caiwu.local-build-fingerprint="$LOCAL_BUILD_FINGERPRINT"', + $dockerfile + ); + $this->assertStringContainsString( + 'node_modules/apexcharts/dist/apexcharts.min.js', + $dockerfile + ); + $this->assertStringContainsString( + 'node_modules/js-cookie/dist/js.cookie.min.js', + $dockerfile + ); + + $this->assertIsString($healthcheck); + foreach ([ + 'public/js/manifest.js', + 'public/js/vendor.js', + 'public/css/app.css', + 'public/mix-manifest.json', + 'public/vendor/apexcharts/apexcharts.min.js', + 'public/vendor/js-cookie/js.cookie.js', + ] as $requiredAsset) { + $this->assertStringContainsString($requiredAsset, $healthcheck); + } + $this->assertStringContainsString( + 'runuser -u www-data -- php artisan app:ready --quiet', + $healthcheck + ); + $this->assertStringContainsString( + 'http://127.0.0.1:18088/auth/login', + $healthcheck + ); + } + + public function test_local_initializer_rebuilds_stale_or_incomplete_images(): void + { + $initializer = file_get_contents(base_path('deploy/local-init.sh')); + + $this->assertIsString($initializer); + $this->assertStringContainsString( + 'calculate_local_image_fingerprint', + $initializer + ); + $this->assertStringContainsString( + 'image_has_required_assets', + $initializer + ); + $this->assertStringContainsString( + '现有镜像缺少必要静态资源', + $initializer + ); + $this->assertStringContainsString( + '--build-arg "LOCAL_BUILD_FINGERPRINT=$local_image_fingerprint"', + $initializer + ); + $this->assertStringContainsString( + '构建期间检测到源码变化,自动按最新源码再构建一次', + $initializer + ); + $this->assertStringContainsString( + '本地镜像缺少必要静态资源,请查看上方构建日志', + $initializer + ); + } + + public function test_missing_static_files_are_not_routed_through_laravel(): void + { + $apache = file_get_contents(base_path('deploy/apache-vhost.conf')); + + $this->assertIsString($apache); + $this->assertStringContainsString( + 'RewriteCond %{REQUEST_URI} ^/(public/|vendor/livewire/)', + $apache + ); + $this->assertStringContainsString( + 'RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f', + $apache + ); + $this->assertStringContainsString( + 'RewriteRule ^ - [R=404,L]', + $apache + ); + } + + public function test_apexcharts_is_loaded_only_by_chart_views(): void + { + foreach ([ + 'admin', + 'portal', + 'print', + ] as $layout) { + $scripts = file_get_contents( + resource_path("views/components/layouts/{$layout}/scripts.blade.php") + ); + + $this->assertIsString($scripts); + $this->assertStringContainsString("@stack('chart_libraries')", $scripts); + $this->assertStringNotContainsString( + "asset('public/vendor/apexcharts/apexcharts.min.js')", + $scripts + ); + } + + $loader = file_get_contents( + resource_path('views/components/charts/apexcharts-loader.blade.php') + ); + + $this->assertIsString($loader); + $this->assertStringContainsString( + "asset('public/vendor/apexcharts/apexcharts.min.js')", + $loader + ); + + $renderedLoader = Blade::render(<<<'BLADE' + @include('components.charts.apexcharts-loader') + @include('components.charts.apexcharts-loader') + @stack('chart_libraries') + BLADE); + + $this->assertSame( + 1, + substr_count($renderedLoader, 'public/vendor/apexcharts/apexcharts.min.js') + ); + + foreach ([ + 'views/widgets/line_chart.blade.php', + 'views/widgets/donut_chart.blade.php', + 'views/widgets/cash_flow.blade.php', + 'views/widgets/bar_chart.blade.php', + 'views/components/reports/summary/chart.blade.php', + ] as $chartView) { + $view = file_get_contents(resource_path($chartView)); + + $this->assertIsString($view); + $this->assertStringContainsString( + "@include('components.charts.apexcharts-loader')", + $view + ); + } + } + + public function test_views_and_component_styles_do_not_depend_on_quicksand(): void + { + $headFiles = glob( + resource_path('views/components/layouts/*/head.blade.php') + ); + + $this->assertIsArray($headFiles); + $this->assertNotEmpty($headFiles); + + foreach ($headFiles as $headFile) { + $head = file_get_contents($headFile); + + $this->assertIsString($head); + $this->assertStringNotContainsString( + 'public/vendor/quicksand/css/quicksand.css', + $head + ); + } + + foreach ([ + base_path('config/apexcharts.php'), + public_path('css/third_party/vue-html-editor_custom.css'), + ] as $componentStyle) { + $contents = file_get_contents($componentStyle); + + $this->assertIsString($contents); + $this->assertStringNotContainsString('Quicksand', $contents); + $this->assertStringContainsString('JetBrains Mono', $contents); + } + } +} diff --git a/tests/Unit/Deployment/WebProcessOwnershipTest.php b/tests/Unit/Deployment/WebProcessOwnershipTest.php index 109aa5ba..be371630 100644 --- a/tests/Unit/Deployment/WebProcessOwnershipTest.php +++ b/tests/Unit/Deployment/WebProcessOwnershipTest.php @@ -30,7 +30,7 @@ class WebProcessOwnershipTest extends TestCase $this->assertIsString($compose); $this->assertStringContainsString( - 'test: ["CMD", "runuser", "-u", "www-data", "--", "php", "artisan", "app:ready", "--quiet"]', + 'test: ["CMD", "jiangling-healthcheck"]', $compose ); $this->assertMatchesRegularExpression(