fix: 合并本地静态资源性能修复

This commit is contained in:
WuYongJie 2026-07-27 15:53:35 +08:00
parent 58528c16b9
commit b5097cd2df
32 changed files with 404 additions and 62 deletions

View File

@ -167,7 +167,7 @@
- 云数据库连接与系统数据库配置必须分开。显式开启 `UPSTREAM_STORED_CONFIG_ENABLED` 后,云数据库密码在 `system_database_connections` 中同时保留旧版 `APP_KEY` 密文用于紧急回滚,并使用系统数据库登录密码派生的密钥写入 `shared_password` 供受信任开发机共享;真实值仍不得进入代码、提交、PR、共享文档或日志。系统数据库固定主机、端口、库名和账号以密文团队资料保存在代码中,不含数据库密码;新人页面只允许填写系统数据库密码,由它解锁固定资料并自动启用 TLS。禁止恢复主机、端口、账号、TLS、证书或 CA 路径等新人表单。系统数据库密码轮换前必须先完成共享云数据库密文重加密、数据库备份和团队连接资料密文更新。只有隔离排错或尚未迁移的旧环境才允许显式开启本机覆盖,并通过私有 `DB_<来源>_*` 环境变量提供完整连接。中继开发机只保留已收紧 ACL 的隧道端点配置,不保存数据库密码。 - 云数据库连接与系统数据库配置必须分开。显式开启 `UPSTREAM_STORED_CONFIG_ENABLED` 后,云数据库密码在 `system_database_connections` 中同时保留旧版 `APP_KEY` 密文用于紧急回滚,并使用系统数据库登录密码派生的密钥写入 `shared_password` 供受信任开发机共享;真实值仍不得进入代码、提交、PR、共享文档或日志。系统数据库固定主机、端口、库名和账号以密文团队资料保存在代码中,不含数据库密码;新人页面只允许填写系统数据库密码,由它解锁固定资料并自动启用 TLS。禁止恢复主机、端口、账号、TLS、证书或 CA 路径等新人表单。系统数据库密码轮换前必须先完成共享云数据库密文重加密、数据库备份和团队连接资料密文更新。只有隔离排错或尚未迁移的旧环境才允许显式开启本机覆盖,并通过私有 `DB_<来源>_*` 环境变量提供完整连接。中继开发机只保留已收紧 ACL 的隧道端点配置,不保存数据库密码。
- 本地开发的标准入口只有“系统设置 → 数据库管理 → 系统数据库”。开发者只填写并验证同一套系统数据库密码,其他连接参数由项目自动设置;成功后系统用户、侧边栏、系统设置和云数据库登记必须继续使用该系统数据库。不得再把主业务连接的 `DB_HOST` 改回本机 MySQL 容器,否则会形成一套互不相干的本地数据,导致用户、配置、来源映射和业务结果与团队不一致。 - 本地开发的标准入口只有“系统设置 → 数据库管理 → 系统数据库”。开发者只填写并验证同一套系统数据库密码,其他连接参数由项目自动设置;成功后系统用户、侧边栏、系统设置和云数据库登记必须继续使用该系统数据库。不得再把主业务连接的 `DB_HOST` 改回本机 MySQL 容器,否则会形成一套互不相干的本地数据,导致用户、配置、来源映射和业务结果与团队不一致。
- 两个现有来源的标识和数据库映射固定为:`jiansun` 对应 `JianSun` 旧业务库,`operations_replica` 对应 `JSReportReplica` 的 `rpt` 只读视图。不得把 `shujuku`、本机测试库名或历史截图中的临时名称写回共享记录,也不得交换两个来源的账号或数据库名。 - 两个现有来源的标识和数据库映射固定为:`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` 时应重新运行标准初始化脚本,而不是操作共享数据库。 - 本机临时引导库的新增迁移由 `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` 来掩盖共享配置读取错误。 - 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 网络代理。本地开发应用直接使用系统数据库中的连接资料访问两套上游;前提是该开发机网络本身能够到达数据库端点。 - 系统数据库统一保存应用数据、云数据库配置、上游数据字典和生产共享快照,但不是 SQL Server 网络代理。本地开发应用直接使用系统数据库中的连接资料访问两套上游;前提是该开发机网络本身能够到达数据库端点。

View File

@ -38,7 +38,7 @@ http://127.0.0.1:18088
下面的命令由 Codex 在 Git Bash、WSL、macOS 或 Linux 终端中执行,不交给新人: 下面的命令由 Codex 在 Git Bash、WSL、macOS 或 Linux 终端中执行,不交给新人:
```bash ```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`,也不得对团队共享系统数据库自行迁移。 拉取新代码后再次运行该脚本时,本机临时引导库会自动更新;连接团队共享系统数据库后,这项自动更新会安全跳过。普通开发者任何时候都不需要执行 `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 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 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、配置、模块、页面模板和路由目录已经挂载到容器,通常清理缓存或重启应用即可; - PHP、配置、模块、页面模板和路由目录已经挂载到容器,通常清理缓存或重启应用即可;
- Composer 依赖、前端依赖、Dockerfile 或构建资源发生变化时,执行 `bash deploy/local-init.sh --build`; - Composer 依赖、前端依赖、Dockerfile 或构建资源发生变化时,重新执行 `bash deploy/local-init.sh`,脚本会自动重建;
- 修改 `runtime/.env` 后,执行: - 修改 `runtime/.env` 后,执行:
```bash ```bash

View File

@ -9,7 +9,7 @@
1. 启动 Docker Desktop; 1. 启动 Docker Desktop;
2. 在系统数据库页面输入负责人提供的系统数据库密码。 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 不得把这些命令、迁移或排错步骤转交给新人执行。
首次登录账号由初始化脚本生成并保存在本机私有凭据文件中。连接系统数据库成功后,临时登录会自动失效,改用系统数据库中属于自己的正式账号登录。 首次登录账号由初始化脚本生成并保存在本机私有凭据文件中。连接系统数据库成功后,临时登录会自动失效,改用系统数据库中属于自己的正式账号登录。

View File

@ -13,7 +13,7 @@ return [
'zoom' => [ 'zoom' => [
'enabled' => false, 'enabled' => false,
], ],
'fontFamily' => '"Quicksand", sans-serif', 'fontFamily' => '"JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace',
'foreColor' => '#595959', 'foreColor' => '#595959',
], ],

View File

@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1.7
# 本地部署用 Dockerfile # 本地部署用 Dockerfile
# 基于 deploy/Dockerfile,额外安装 composer 依赖与前端构建产物 # 基于 deploy/Dockerfile,额外安装 composer 依赖与前端构建产物
# 由 docker-compose.local.yml 使用 # 由 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 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 . /var/www/html
COPY deploy/apache-vhost.conf /etc/apache2/sites-available/000-default.conf 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/php.ini /usr/local/etc/php/conf.d/jiangling.ini
COPY deploy/entrypoint.sh /usr/local/bin/jiangling-entrypoint COPY deploy/entrypoint.sh /usr/local/bin/jiangling-entrypoint
COPY deploy/local-healthcheck.sh /usr/local/bin/jiangling-healthcheck
# ---------------- 安装 PHP 依赖(本地开发模式,包含 PHPUnit 等测试工具)---------------- RUN composer dump-autoload --no-scripts --optimize
RUN composer install --prefer-dist \
--no-interaction \
--no-scripts \
--no-progress \
--no-ansi \
&& 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 # 提升 node 堆内存上限;dev 模式无压缩,避免 mix/webpack 生产构建 OOM
ENV NODE_OPTIONS="--max-old-space-size=2048" 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 \ && mkdir -p \
storage/app/public \ storage/app/public \
storage/app/temp \ storage/app/temp \
@ -100,5 +120,10 @@ RUN chmod +x /usr/local/bin/jiangling-entrypoint \
bootstrap/cache \ bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache public && 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"] ENTRYPOINT ["jiangling-entrypoint"]
CMD ["apache2-foreground"] CMD ["apache2-foreground"]

View File

@ -9,6 +9,13 @@ ServerName localhost
Require all granted Require all granted
</Directory> </Directory>
# 静态文件缺失时必须快速返回 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 SetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1
ErrorLog ${APACHE_LOG_DIR}/error.log ErrorLog ${APACHE_LOG_DIR}/error.log

View File

@ -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

View File

@ -64,6 +64,55 @@ set_env_value() {
chmod 600 "$environment_file" 2>/dev/null || true 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)" echo "==> 工作目录: $(pwd)"
if ! command -v docker >/dev/null 2>&1; then 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 image_exists=1
fi fi
if [ "$build_mode" = "build" ] || { [ "$build_mode" = "auto" ] && [ "$image_exists" -eq 0 ]; }; then local_image_fingerprint="$(calculate_local_image_fingerprint)"
echo "==> [1/5] 构建本地开发镜像..." stored_image_fingerprint=""
docker compose -f "$compose_file" build app image_contract_ok=0
elif [ "$build_mode" = "skip" ] && [ "$image_exists" -eq 0 ]; then
echo "错误:本地开发镜像不存在,不能使用 --skip-build。" >&2 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 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 else
echo "==> [1/5] 复用现有本地开发镜像。需要更新依赖时请使用 --build。" echo "==> [1/5] 本地开发镜像与当前源码一致,直接复用。"
fi fi
echo "==> [2/5] 启动本地容器..." echo "==> [2/5] 启动本地容器..."

View File

@ -3,7 +3,7 @@
# - mysql 只用于首次登录和填写“系统数据库”前的临时引导 # - 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 down
# 日志: docker compose -f docker-compose.local.yml logs -f app # 日志: docker compose -f docker-compose.local.yml logs -f app
# #
@ -35,7 +35,7 @@ services:
networks: networks:
- jiangling_net - jiangling_net
healthcheck: healthcheck:
test: ["CMD", "runuser", "-u", "www-data", "--", "php", "artisan", "app:ready", "--quiet"] test: ["CMD", "jiangling-healthcheck"]
interval: 15s interval: 15s
timeout: 5s timeout: 5s
retries: 10 retries: 10

View File

@ -289,7 +289,7 @@ JIANSUN_SLOW_QUERY_COOLDOWN=300
本地首次启动时页面必须明确显示“本地临时引导库”,不能把本机 MySQL 标成财务系统配置数据库。开发者连接共享系统数据库时只输入密码;系统自动解锁固定地址、端口、库名、账号和 TLS 配置,只检查数据库能够连接且基础系统表存在。迁移版本、管理员权限映射、公司映射和云数据库共享密码均不得作为连接拦截条件。连接成功后立即清除临时账号 Session,并要求使用共享库正式账号重新登录;`super_admin` 和 `admin` 角色即使缺少单项权限关联,也不得被后台权限中间件阻挡。 本地首次启动时页面必须明确显示“本地临时引导库”,不能把本机 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。 系统数据库连接测试从应用容器发起,但新人页面只显示密码输入框。主机、端口、库名、账号和 TLS 均来自使用系统数据库密码加密的团队固定连接资料;不得把这些高级连接项重新暴露给普通开发者。密码错误必须回到原页面显示中文提示,网络或数据库暂不可用也必须保留旧连接并显示中文原因,不得出现 500。

View File

@ -3,7 +3,7 @@
} }
.ql-container { .ql-container {
font-family: "Quicksand", sans-serif !important; font-family: "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace !important;
} }
.ql-editor p { .ql-editor p {
@ -285,4 +285,4 @@
.ql-snow .ql-hidden { .ql-snow .ql-hidden {
display: none !important; display: none !important;
} }

View File

@ -0,0 +1,5 @@
@once
@push('chart_libraries')
<script src="{{ asset('public/vendor/apexcharts/apexcharts.min.js') }}"></script>
@endpush
@endonce

View File

@ -31,9 +31,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css//third_party/vue-html-editor.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/vue-html-editor.css?v=' . version('short')) }}" type="text/css">

View File

@ -8,7 +8,7 @@
@stack('scripts_start') @stack('scripts_start')
<script src="{{ asset('public/vendor/apexcharts/apexcharts.min.js') }}"></script> @stack('chart_libraries')
@stack('charts') @stack('charts')

View File

@ -24,9 +24,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css/app.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/app.css?v=' . version('short')) }}" type="text/css">
@ -58,4 +55,3 @@
@stack('head_end') @stack('head_end')
</head> </head>

View File

@ -22,9 +22,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css">

View File

@ -22,9 +22,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css">

View File

@ -12,7 +12,6 @@
<base href="{{ config('app.url') . '/' }}"> <base href="{{ config('app.url') . '/' }}">
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css/app.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/app.css?v=' . version('short')) }}" type="text/css">
@stack('css') @stack('css')

View File

@ -24,9 +24,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css//third_party/vue-html-editor.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/vue-html-editor.css?v=' . version('short')) }}" type="text/css">

View File

@ -24,9 +24,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css">

View File

@ -8,7 +8,7 @@
@stack('scripts_start') @stack('scripts_start')
<script src="{{ asset('public/vendor/apexcharts/apexcharts.min.js') }}"></script> @stack('chart_libraries')
@stack('charts') @stack('charts')

View File

@ -24,9 +24,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css">

View File

@ -8,7 +8,7 @@
@stack('scripts_start') @stack('scripts_start')
<script src="{{ asset('public/vendor/apexcharts/apexcharts.min.js') }}"></script> @stack('chart_libraries')
@stack('charts') @stack('charts')

View File

@ -24,9 +24,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css//third_party/swiper-bundle.min.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css">

View File

@ -20,9 +20,6 @@
<!--Icons--> <!--Icons-->
<link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/fonts/material-icons/style.css?v=' . version('short')) }}" type="text/css">
<!-- Font -->
<link rel="stylesheet" href="{{ asset('public/vendor/quicksand/css/quicksand.css?v=' . version('short')) }}" type="text/css">
<!-- Css --> <!-- Css -->
<link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/element.css?v=' . version('short')) }}" type="text/css">
<link rel="stylesheet" href="{{ asset('public/css/app.css?v=' . version('short')) }}" type="text/css"> <link rel="stylesheet" href="{{ asset('public/css/app.css?v=' . version('short')) }}" type="text/css">

View File

@ -37,6 +37,8 @@
</div> </div>
</div> </div>
@include('components.charts.apexcharts-loader')
@push('body_scripts') @push('body_scripts')
{!! $charts['bar']->script() !!} {!! $charts['bar']->script() !!}
{!! $charts['donut']->script() !!} {!! $charts['donut']->script() !!}

View File

@ -8,6 +8,8 @@
</div> </div>
</div> </div>
@include('components.charts.apexcharts-loader')
@push('body_scripts') @push('body_scripts')
{!! $chart->script() !!} {!! $chart->script() !!}
@endpush @endpush

View File

@ -48,6 +48,8 @@
</div> </div>
</div> </div>
@include('components.charts.apexcharts-loader')
@push('body_scripts') @push('body_scripts')
{!! $chart->script() !!} {!! $chart->script() !!}
@endpush @endpush

View File

@ -8,6 +8,8 @@
</div> </div>
</div> </div>
@include('components.charts.apexcharts-loader')
@push('body_scripts') @push('body_scripts')
{!! $chart->script() !!} {!! $chart->script() !!}
@endpush @endpush

View File

@ -8,6 +8,8 @@
</div> </div>
</div> </div>
@include('components.charts.apexcharts-loader')
@push('body_scripts') @push('body_scripts')
{!! $chart->script() !!} {!! $chart->script() !!}
@endpush @endpush

View File

@ -0,0 +1,187 @@
<?php
namespace Tests\Unit\Deployment;
use Illuminate\Support\Facades\Blade;
use Tests\TestCase;
class LocalStaticAssetsTest extends TestCase
{
public function test_local_image_builds_and_checks_required_static_assets(): void
{
$dockerfile = file_get_contents(base_path('deploy/Dockerfile.local'));
$healthcheck = file_get_contents(base_path('deploy/local-healthcheck.sh'));
$this->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);
}
}
}

View File

@ -30,7 +30,7 @@ class WebProcessOwnershipTest extends TestCase
$this->assertIsString($compose); $this->assertIsString($compose);
$this->assertStringContainsString( $this->assertStringContainsString(
'test: ["CMD", "runuser", "-u", "www-data", "--", "php", "artisan", "app:ready", "--quiet"]', 'test: ["CMD", "jiangling-healthcheck"]',
$compose $compose
); );
$this->assertMatchesRegularExpression( $this->assertMatchesRegularExpression(