chore: 前端部署去掉 nginx,静态由 serve,TLS 交 Traefik
Some checks failed
Build and Deploy Front / deploy (push) Failing after 32m20s
Some checks failed
Build and Deploy Front / deploy (push) Failing after 32m20s
生产不再内置 nginx/`__cos__`;镜像用 serve -s,HTTPS 由 Traefik+cert-manager;面单预览走 stream。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
893adc0d56
commit
c6723ab476
@ -33,7 +33,7 @@ alwaysApply: true
|
||||
- HandsontablePro 固定列序:操作(可选,内容自适应宽)→ 序号(50px)→ 复选框(可选,60px)→ 数据;`stretch-columns` 只拉宽数据列,勿再传 `action-column-position` / `action-column-width`
|
||||
- 列表页高度:`flex: 1; min-height: 0`,`.list-wrap` 勿写死 min-height
|
||||
- 分页 10/30/50/100,默认 50
|
||||
- 新页面 path 与 API 前缀冲突时须配置 nginx SPA 回退(见规范 §十五)
|
||||
- 生产前后端分域名 + Traefik TLS;前端容器仅静态 SPA(见规范 §十五)
|
||||
|
||||
### 权限
|
||||
- `hasPermission('Module.Resource.Action')` 控制按钮/路由
|
||||
|
||||
@ -26,7 +26,5 @@ xhs-agent-desktop
|
||||
.env*
|
||||
!.env.production
|
||||
|
||||
# 本地/旧部署用的 nginx,k3s 用 nginx-k3s.conf
|
||||
nginx-default.conf
|
||||
nginx-docker.conf
|
||||
# 本地简易镜像另用 Dockerfile;k3s 构建用 Dockerfile.k3s
|
||||
Dockerfile
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@ -1,9 +1,9 @@
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
# 静态资源
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
|
||||
# Docker 内只用 HTTP:80,SSL 在宿主机 nginx 或网关层处理
|
||||
COPY nginx-docker.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 本地/简易 Docker:仅静态 SPA。HTTPS 由外层网关(如 Traefik)终结。
|
||||
# 构建前先执行:pnpm build
|
||||
FROM docker.m.daocloud.io/library/node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN npm config set registry https://registry.npmmirror.com \
|
||||
&& npm install -g serve@14.2.4
|
||||
COPY dist/ ./dist/
|
||||
EXPOSE 80
|
||||
CMD ["serve", "-s", "dist", "-l", "80"]
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
# k3s / Kaniko:静态 SPA(方案 A)。API 由浏览器按站点域名直连
|
||||
# k3s / Kaniko:静态 SPA(方案 A)。TLS 由 Traefik(websecure) + cert-manager 终结。
|
||||
# API 由浏览器按站点域名直连:
|
||||
# test.ideship.com → api.test.ideship.com;其余 → api.ideship.com
|
||||
# FRONT_BUILD_SCRIPT:
|
||||
# prod 默认 build(.env.production → api.ideship.com)
|
||||
@ -16,7 +17,11 @@ RUN pnpm config set registry https://registry.npmmirror.com \
|
||||
COPY . .
|
||||
RUN pnpm run ${FRONT_BUILD_SCRIPT}
|
||||
|
||||
FROM docker.m.daocloud.io/library/nginx:1.27-alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html/
|
||||
COPY nginx-k3s.conf /etc/nginx/conf.d/default.conf
|
||||
# 仅静态托管 + SPA fallback;不再内置 nginx(网关层 Traefik 负责 HTTPS)
|
||||
FROM docker.m.daocloud.io/library/node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN npm config set registry https://registry.npmmirror.com \
|
||||
&& npm install -g serve@14.2.4
|
||||
COPY --from=build /app/dist ./dist
|
||||
EXPOSE 80
|
||||
CMD ["serve", "-s", "dist", "-l", "80"]
|
||||
|
||||
@ -98,17 +98,18 @@ npm run lint
|
||||
|
||||
1. 安装依赖 `pnpm i`
|
||||
2. 构建 `pnpm build`(生成 `dist/`)
|
||||
3. **宿主机 nginx(推荐)**:将 `dist/` 放到 `/usr/share/nginx/html`,使用 `nginx-default.conf`(含 SSL,证书在 `/etc/nginx/ssl/`)
|
||||
4. **Docker 方式(7962 端口)**:使用 `nginx-docker.conf`(仅 HTTP,不含 SSL)
|
||||
3. **k3s(推荐)**:`Dockerfile.k3s` 多阶段构建,容器内用 `serve` 提供静态 SPA(`:80`);**HTTPS 由 Traefik `websecure` + cert-manager(Let's Encrypt)终结**,经 Ingress(tls) → Service,不在前端容器内挂证书。
|
||||
4. **本地简易 Docker**:先 `pnpm build`,再 `docker build -t web1 .`(`Dockerfile` 拷贝已有 `dist/`)
|
||||
|
||||
```bash
|
||||
# 本地试跑静态容器(HTTP:7962 → 容器 80)
|
||||
docker build -t web1 .
|
||||
docker rm -f web
|
||||
docker run -d -p 7962:80 --name web web1
|
||||
docker ps | grep web # 确认 Up 状态
|
||||
docker ps | grep web
|
||||
```
|
||||
|
||||
> 勿把 `nginx-default.conf`(含 SSL)直接打进 Docker,容器内没有 `/etc/nginx/ssl/` 会导致启动失败。
|
||||
> 生产勿在前端镜像内配置 SSL;证书由集群 cert-manager 写入 Secret,Traefik Ingress 引用即可。
|
||||
|
||||
## 贡献指南
|
||||
|
||||
|
||||
@ -547,7 +547,7 @@ src/views/h5/customer-center/
|
||||
- [x] 独立 H5 账号密码登录(`/h5/login`,不跳 PC 登录页)
|
||||
- [x] H5 微信扫码登录:`GET /wechat/mp/login-qrcode` + 长按识别 + 轮询登录
|
||||
- [x] 独立 H5 充值(`/h5/recharge`,不跳 PC 充值页)
|
||||
- [x] 中英文与 nginx `/h5` SPA 回退
|
||||
- [x] 中英文与 `/h5` SPA 回退(`serve -s` / Traefik)
|
||||
- [ ] 键盘适配与运单上下文在顾问页的完整预填联调
|
||||
- [x] 手机完整下单(二期):`/h5/place-order` 三步精简下单 + 查价「去下单」草稿深链
|
||||
|
||||
|
||||
@ -555,33 +555,25 @@ const { t } = useI18n();
|
||||
- [ ] 分页组件置于**列表上方工具栏右侧**(列表右上角),**§7.3**;分页 10/30/50/100,默认 50
|
||||
- [ ] 操作按钮按权限使用 `hasPermission`
|
||||
- [ ] 路由 meta 配置 `permission`
|
||||
- [ ] **新增前端页面路径**若落在 `/order/`、`/channel/`、`/system/` 等与 Admin API 同源前缀下,须在 **`nginx-default.conf` / `nginx-docker.conf`** 增加 `^~` 的 `try_files /index.html =200` 规则(**§十五**),避免刷新/直链 404
|
||||
- [ ] **部署**:前端与 Admin API **分域名**(如 `ideship.com` / `api.ideship.com`);TLS 走 Traefik + cert-manager(**§十五**)
|
||||
- [ ] 文案通过 i18n,不写死中文
|
||||
- [ ] 命名符合本规范(文件、路由、变量、i18n key)
|
||||
|
||||
---
|
||||
|
||||
## 十五、部署与 nginx(SPA 路由)
|
||||
## 十五、部署与网关(Traefik / SPA)
|
||||
|
||||
生产环境 nginx 将 `/order/`、`/channel/`、`/system/` 等前缀**同时**用于 Admin API 反代。浏览器刷新或直接打开前端 URL(如 `/order/batchJobs`)时,若未单独配置 SPA 回退,请求会被转发到后端 API 并返回 **404**。
|
||||
生产已弃用站内 nginx。静态资源由前端容器(`serve -s`)提供,**HTTPS** 由 k3s Traefik `websecure` + cert-manager(Let's Encrypt)签发;流量路径:
|
||||
|
||||
`浏览器 → :443 Traefik(websecure) → Ingress(tls) → Service → 前端 Pod(:80)`
|
||||
|
||||
**约定:**
|
||||
|
||||
1. 官网 **`/`** 为 Vue 路由(`src/views/site`);静态资源在 **`/site/`**;**`/9610.html`** 为 `public/9610.html`。客户登录为 **`/login`**,管理端为 **`/admin`**。勿再把 `/login` 重定向到 `/`。
|
||||
2. 每新增一个**前端页面路由**,若其 path 与 API 前缀冲突,必须在 `nginx-default.conf` 与 `nginx-docker.conf` 中、**API 正则代理块之前**增加更高优先级的 `^~` 规则,例如:
|
||||
|
||||
```nginx
|
||||
# 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先)
|
||||
location ^~ /order/batchJobs {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
```
|
||||
|
||||
3. 修改后执行 `nginx -t && nginx -s reload`(或重建前端 Docker 镜像使 `nginx-docker.conf` 生效)。
|
||||
|
||||
4. 本地 Vite 开发不受此限制;**仅在部署环境**需遵守本节前缀规则。`pnpm dev` 时 `/` 即为官网。
|
||||
|
||||
当前订单模块已配置的前端路径示例:`/order/priceCheck`、`/order/placeOrder`、`/order/myOrders`、`/order/batchJobs`。
|
||||
1. **前后端分域名**(方案 A):站点如 `ideship.com` / `test.ideship.com`,API 如 `api.ideship.com` / `api.test.ideship.com`。浏览器直连 API,前端容器不做 Admin 反代,也无 `/__cos__` 反代。
|
||||
2. SPA 回退由 `serve -s`(或等价静态服务器)处理;刷新 `/order/batchJobs` 等路径应回 `index.html`,不依赖 nginx `try_files`。
|
||||
3. 官网 **`/`** 为 Vue 路由(`src/views/site`);静态资源在 **`/site/`**;**`/9610.html`** 为 `public/9610.html`。客户登录 **`/login`**,管理端 **`/admin`**。
|
||||
4. 面单/附件预览:开发环境可用 Vite `/__cos__`;**生产**经鉴权 `file-storage/stream` → blob,勿直开 `https://cos.*`(自定义域证书常不匹配)。
|
||||
5. 本地 `pnpm dev` 不受 Ingress 限制;`/` 即为官网。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -181,7 +181,7 @@
|
||||
| `OmsFileUploadHelper` | 聊天图片上传(新 folder:`cs`) |
|
||||
| `NoticeService` | 可选:新消息站内信 |
|
||||
| 数据权限 | 坐席列表/认领范围按客服数据权限过滤 |
|
||||
| nginx | P1 必须补 WebSocket Upgrade |
|
||||
| Traefik Ingress | P1 须允许 WebSocket Upgrade(SignalR) |
|
||||
|
||||
### 4.3 技术选型
|
||||
|
||||
@ -197,16 +197,8 @@
|
||||
### 4.4 服务端部署注意
|
||||
|
||||
1. `AddSignalR().AddStackExchangeRedis(...)` 或与现有 FreeRedis 兼容的 backplane 方案(实现阶段选定一种,避免双 Redis 客户端混乱)。
|
||||
2. nginx 为 Hub 增加独立 location(建议 `/hubs/`),配置:
|
||||
|
||||
```nginx
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
```
|
||||
|
||||
3. 前端 Docker / `nginx-docker.conf`、`nginx-default.conf` 同步修改;新坐席路由若与 API 前缀冲突,按规范 §十五做 SPA `try_files`。
|
||||
2. Traefik / Ingress 对 Hub 路径(建议 `/hubs/` 或 SignalR 约定路径)开启 WebSocket;超时按长连接调大。前后端分域名时,浏览器直连 **api.***,前端静态容器无需反代 Hub。
|
||||
3. SPA 回退由前端容器 `serve -s` 处理;见规范 §十五。
|
||||
|
||||
---
|
||||
|
||||
@ -610,7 +602,7 @@ src/composables/useCsHub.ts
|
||||
- [ ] 坐席右侧能看到客户与最近订单摘要
|
||||
- [ ] 结束会话后客户再进线为新会话
|
||||
- [ ] 双实例部署下消息不丢(Redis backplane)
|
||||
- [ ] nginx WebSocket 升级验证通过
|
||||
- [ ] Traefik / Ingress WebSocket 升级验证通过
|
||||
- [ ] 权限码控制认领 / 回复 / 结束;客户无法调坐席接口
|
||||
|
||||
### 12.3 非功能
|
||||
@ -630,7 +622,7 @@ src/composables/useCsHub.ts
|
||||
| 2 | DDL:`t_cs_conversation` / `t_cs_message` + 仓储 | P1 |
|
||||
| 3 | HTTP 会话/消息/上传 API + 权限码种子 | P1 |
|
||||
| 4 | 坐席 Inbox 静态联调(可先轮询) | P1 |
|
||||
| 5 | SignalR Hub + 前端 `useCsHub` + nginx | P1 |
|
||||
| 5 | SignalR Hub + 前端 `useCsHub` + Traefik WS | P1 |
|
||||
| 6 | 分配逻辑 + 在线心跳 + 微信降级 | P1 |
|
||||
| 7 | AI 转人工 + 上下文侧栏 | P1 |
|
||||
| 8 | 未读角标 +(可选)站内信 | P1 |
|
||||
@ -646,7 +638,7 @@ src/composables/useCsHub.ts
|
||||
| 风险 | 对策 |
|
||||
| --- | --- |
|
||||
| 坐席不用系统、仍只回微信 | P0 微信永远保留;P1 强调「转人工带单号」提升系统内处理价值 |
|
||||
| SignalR 经 nginx 失败 | 独立 `/hubs/` location + 验收清单;可临时 `SignalREnabled=false` HTTP 轮询 |
|
||||
| SignalR 经网关失败 | Ingress 允许 Upgrade + 验收清单;可临时 `SignalREnabled=false` HTTP 轮询 |
|
||||
| 多实例串消息 | 强制 Redis backplane,禁止单机假设 |
|
||||
| 客户把客服当即时 IM 闲聊 | 入口文案强调「物流咨询」;系统消息提示工作时间(可配置) |
|
||||
| AI 灌入消息过长 | 限制条数与单条长度;仅转人工时写入 |
|
||||
|
||||
@ -46,7 +46,7 @@ pnpm install # 首次
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
生产构建:`pnpm build`,Nginx 配置见 `nginx-default.conf`(**仅** SPA 页面路径走 `index.html`,`/xhs/agent/list` 等 API 走 admin 后端,勿整段 `/xhs` 当 SPA)。
|
||||
生产构建:`pnpm build`;前后端分域名后,`/xhs/*` 页面由前端 `serve -s` SPA 回退,`/xhs/agent/*` 等 API 走 **api.*** Admin,勿把 API 打到前端域名。
|
||||
|
||||
### 1.4 LIKE AI Agent
|
||||
|
||||
@ -158,12 +158,12 @@ Content-Type: application/json
|
||||
- [ ] **F2** 编辑提供商:BaseUrl、Model、ApiSecret 保存成功
|
||||
- [ ] **F3** 对话发送消息 → 流式/完整回复正常
|
||||
|
||||
### 阶段 G — 生产/nginx(上线前)
|
||||
### 阶段 G — 生产 / Traefik(上线前)
|
||||
|
||||
- [ ] **G1** `nginx -t && nginx -s reload` 已执行
|
||||
- [ ] **G2** 刷新 `/xhs/analysis` 等页面返回 SPA,**不** 404
|
||||
- [ ] **G3** `POST https://api.nanafa.com/admin/xhs/agent/list`(带 Token)正常
|
||||
- [ ] **G4** Agent 使用生产 `--oms-url https://api.nanafa.com/admin`
|
||||
- [ ] **G1** Ingress(tls) + cert-manager 证书就绪;前端 Service 健康
|
||||
- [ ] **G2** 刷新站点域名下 `/xhs/analysis` 等返回 SPA,**不** 404
|
||||
- [ ] **G3** `POST https://api.ideship.com/admin/xhs/agent/list`(带 Token)正常
|
||||
- [ ] **G4** Agent 使用生产 `--oms-url https://api.ideship.com/admin`
|
||||
|
||||
---
|
||||
|
||||
@ -232,8 +232,8 @@ curl.exe -s "http://localhost:5281/admin/rbac/auth/login" ...
|
||||
|
||||
### 5.5 前端 dev 正常、生产 404
|
||||
|
||||
- Nginx 仅对 **页面路径** 回退 `index.html`(见 `nginx-default.conf` 第 63–66 行)
|
||||
- API 请求应走 `VITE_API_ADMIN_URL`(如 `https://api.nanafa.com/admin`),不要打到 oms 静态站点的 `/xhs/agent/list`
|
||||
- 站点域名下页面由 `serve -s` 回退 `index.html`
|
||||
- API 请求应走 `VITE_API_ADMIN_URL`(如 `https://api.ideship.com/admin`),不要打到前端静态域名的 `/xhs/agent/list`
|
||||
|
||||
### 5.6 AI 生成 / 分析失败
|
||||
|
||||
|
||||
@ -126,18 +126,12 @@ export default XHS;
|
||||
'menu.system.xhsAiAssistant': 'AI 助手',
|
||||
```
|
||||
|
||||
### 2.3 Vite / Nginx SPA 规则
|
||||
### 2.3 Vite / SPA 规则
|
||||
|
||||
`/xhs/*` 为纯前端路由,须加入:
|
||||
`/xhs/*` 为纯前端路由:
|
||||
|
||||
- `vite.config.ts` → `frontendRoutes` 增加 `'/xhs'`
|
||||
- `nginx-default.conf` → 在 API 反代规则之前增加:
|
||||
|
||||
```nginx
|
||||
location ~ ^/xhs {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
```
|
||||
- `vite.config.ts` → `frontendRoutes` 增加 `'/xhs'`(开发 history fallback)
|
||||
- 生产由前端容器 `serve -s` 回退 `index.html`;API 走分域名 `api.*`,无需站内 nginx
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -422,17 +422,10 @@ src/SimpleAdmin.Api/
|
||||
| Token | 与数据库/AppSettings 一致 |
|
||||
| 消息加解密 | **明文**(当前实现) |
|
||||
|
||||
### 7.3 Nginx
|
||||
### 7.3 网关 / 回调
|
||||
|
||||
`nginx-default.conf` 关键规则:
|
||||
|
||||
```nginx
|
||||
# 微信回调
|
||||
location ^~ /api/apiwechat/ {
|
||||
rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break;
|
||||
proxy_pass https://api.nanafa.com;
|
||||
}
|
||||
```
|
||||
生产已弃用站内 nginx。微信服务器回调应直连 **API 域名**(如 `https://api.ideship.com/admin/apiwechat/verification`),由 Traefik Ingress 转发到后端 Service;前端静态容器不做 `/api/apiwechat` 反代。
|
||||
本地开发仍可由 Vite 代理 `/api/apiwechat` → Admin。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,489 +0,0 @@
|
||||
# ============ 生产环境部署必读(避免「本地可以、线上不行」)============
|
||||
# 1. 本文件必须真正生效:确认 oms.nanafa.com 的请求由「本 server 块」处理
|
||||
# - 若用 include:确保 include 的是含本 server 的 conf,且无其它 server 抢先匹配
|
||||
# - 更新后必须执行:nginx -t && nginx -s reload
|
||||
# 2. /api、/Track、/rbac、/order 等 Admin 接口均由 nginx 代理到 api.nanafa.com,浏览器只访问 oms 同源
|
||||
# 3. HTTPS:证书部署到服务器(勿提交 .key 到 git)
|
||||
# /etc/nginx/ssl/oms.nanafa.com.pem
|
||||
# /etc/nginx/ssl/oms.nanafa.com.key
|
||||
# 4. 官网 `/` 走 Vue SPA;静态资源在 /site/;客户登录 /login;管理端 /admin
|
||||
# 5. 静态资源使用 /login-assets/,勿使用 /login/ 目录名
|
||||
# 6. 微信公众号服务器 URL 请使用 https://api.ideship.com/admin/apiwechat/verification
|
||||
# (生产三套:admin / itfx / track,均挂在 https://api.ideship.com/{pathbase})
|
||||
# ============================================================================
|
||||
|
||||
# HTTP → HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name oms.nanafa.com;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name oms.nanafa.com;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/oms.nanafa.com.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/oms.nanafa.com.key;
|
||||
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:OMS_SSL:10m;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# 启用gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/xml image/svg+xml;
|
||||
gzip_comp_level 6;
|
||||
gzip_disable "msie6";
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# ============ 官网首页(Vue SPA)============
|
||||
location = / {
|
||||
try_files /index.html =200;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
location = /9610.html {
|
||||
try_files /9610.html =404;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# 官网仅静态资源在 /site/;首页保持在 /
|
||||
location = /site {
|
||||
return 301 /;
|
||||
}
|
||||
location = /site/ {
|
||||
return 301 /;
|
||||
}
|
||||
location = /site/index.html {
|
||||
return 301 /;
|
||||
}
|
||||
|
||||
location ^~ /site/ {
|
||||
try_files $uri =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /favicon.ico {
|
||||
try_files /favicon.png =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /favicon.png {
|
||||
try_files /favicon.png =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ============ 客户 / 管理端登录入口 ============
|
||||
location = /login {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /login/ {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
rewrite ^/admin/?$ /index.html last;
|
||||
|
||||
location = /admin {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /admin/ {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ API 前端路由(必须先于 /api/ 代理)============
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 小红书前端路由(^~ 须比下方 /xhs/ API 更具体)============
|
||||
location ^~ /xhs/startService {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /xhs/creation {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /xhs/publishHistory {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /xhs/skills {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /xhs/humanSettings {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /xhs/agentPair {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /xhs/analysis {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /xhs/analysis/ {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 微信 H5 / 操作页(^~ 须比下方 API 前缀更长或更具体)============
|
||||
location ^~ /wechat/register {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /wechat/bind {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /h5 {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /operation/inboundScan {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /operation/outboundScan {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /operation/printLabel {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /operation/parcelArrival {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /operation/fulfillPack {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先)============
|
||||
location ^~ /order/priceCheck {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/placeOrder {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/myOrders {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/fulfillOrders {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/waybillManage {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/pickupOrder {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/customs9610Order {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/batchJobs {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/templateMapping {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/platformStore {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /order/platformStoreOrder {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 知识库前端路由(^~ 须比下方 /kb/ API 代理更优先)============
|
||||
location ^~ /kb/category {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /kb/article {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 仓储前端路由(^~ 须比下方 /warehouse/ API 正则代理更优先)============
|
||||
# 页面路径与 API 前缀相同者用精确匹配,避免 /warehouse/sku/list 等接口被误判为 SPA
|
||||
location = /warehouse/parcel {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /warehouse/inventory {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /warehouse/sku {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /warehouse/binLocation {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /warehouse/inboundNotice {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# 采集插件确认页(/Plugin/AddPackage);API 走 /plugin/browser/*
|
||||
location ^~ /Plugin/ {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 渠道前端路由(^~ 须比下方 /channel/ API 正则代理更优先)============
|
||||
location ^~ /channel/salesProducts { try_files /index.html =200; }
|
||||
location ^~ /channel/serviceChannels { try_files /index.html =200; }
|
||||
location ^~ /channel/priceArea { try_files /index.html =200; }
|
||||
location ^~ /channel/blacklist { try_files /index.html =200; }
|
||||
location ^~ /channel/remoteZones { try_files /index.html =200; }
|
||||
location ^~ /channel/weightRules { try_files /index.html =200; }
|
||||
location ^~ /channel/weightRounding { try_files /index.html =200; }
|
||||
location ^~ /channel/autoTrigger { try_files /index.html =200; }
|
||||
location ^~ /channel/issueTemplate { try_files /index.html =200; }
|
||||
location ^~ /channel/labelDesign { try_files /index.html =200; }
|
||||
location ^~ /channel/guide { try_files /index.html =200; }
|
||||
|
||||
# ============ 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)============
|
||||
# 页面路径为 camelCase;API 为 kebab(如 /system/dict/list、/system/batch-email/…),勿用宽泛前缀匹配 dict
|
||||
location = /system/dict {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /system/warehouseAddress {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /system/announcement {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /system/country {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/users {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/roles {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/menus {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/organizations {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/batchEmail {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/otherConfig {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/aiAssistant {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/aiSkills {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/mcpTools {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/dataPerm/ {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 营销前端路由(^~ 须比下方 /marketing/ API 正则代理更优先)============
|
||||
location ^~ /marketing/memberLevel { try_files /index.html =200; }
|
||||
location ^~ /marketing/pointsRule { try_files /index.html =200; }
|
||||
location ^~ /marketing/pointsFlow { try_files /index.html =200; }
|
||||
location ^~ /marketing/couponTemplate { try_files /index.html =200; }
|
||||
location ^~ /marketing/couponGrant { try_files /index.html =200; }
|
||||
location ^~ /marketing/commissionRule { try_files /index.html =200; }
|
||||
location ^~ /marketing/commissionDetail { try_files /index.html =200; }
|
||||
location ^~ /marketing/commissionWithdraw { try_files /index.html =200; }
|
||||
location ^~ /marketing/referralTree { try_files /index.html =200; }
|
||||
|
||||
# ============ 微信公众号服务器回调(admin API,非 itfx)============
|
||||
location ^~ /api/apiwechat/ {
|
||||
rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# ============ API 代理(仅真实后端接口,同源请求避免跨域 405)============
|
||||
location ~ ^/api/ {
|
||||
rewrite ^/api/(.*)$ /itfx/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
location ^~ /Track {
|
||||
rewrite ^/Track(.*)$ /track/Track$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# ============ Admin API 同源反代(/rbac /order /xhs … → api.nanafa.com/admin)============
|
||||
location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse|plugin|marketing|kb)/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location ^~ /wechat/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location ^~ /operation/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location ^~ /xhs/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location ^~ /uploads/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location ^~ /mcp/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location ^~ /ai/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# HTML 不缓存
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
# 微信 JS 安全域名校验:原样返回 txt,禁止 SPA 回落 index.html
|
||||
location ~* ^/MP_verify_[A-Za-z0-9]+\.txt$ {
|
||||
default_type text/plain;
|
||||
charset off;
|
||||
add_header Cache-Control "no-store";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# ============ 静态资源缓存 ============
|
||||
|
||||
location ~* \.(?:js|css)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ~* \.(?:jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ~* \.(?:woff2?|ttf|eot|otf)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ~* \.(?:pdf|zip|tar|gz|br)$ {
|
||||
try_files $uri =404;
|
||||
expires 1M;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ============ SPA 前端路由(放在最后)============
|
||||
# 不用 try_files $uri $uri/ —— 若磁盘存在同名目录且无 index,会 403 且不会回落 index.html
|
||||
location / {
|
||||
index index.html;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
}
|
||||
@ -1,279 +0,0 @@
|
||||
# Docker 容器专用:只监听 80,不配置 SSL(7962:80,由外层网关/宿主机 nginx 终结 HTTPS)
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml image/svg+xml;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = / {
|
||||
try_files /index.html =200;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
location = /9610.html {
|
||||
try_files /9610.html =404;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
location = /site {
|
||||
return 301 /;
|
||||
}
|
||||
location = /site/ {
|
||||
return 301 /;
|
||||
}
|
||||
location = /site/index.html {
|
||||
return 301 /;
|
||||
}
|
||||
|
||||
location ^~ /site/ {
|
||||
try_files $uri =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /favicon.ico {
|
||||
try_files /favicon.png =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /favicon.png {
|
||||
try_files /favicon.png =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /login {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /login/ {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
rewrite ^/admin/?$ /index.html last;
|
||||
|
||||
location = /admin {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /admin/ {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
location ^~ /xhs/startService { try_files /index.html =200; }
|
||||
location ^~ /xhs/creation { try_files /index.html =200; }
|
||||
location ^~ /xhs/publishHistory { try_files /index.html =200; }
|
||||
location ^~ /xhs/skills { try_files /index.html =200; }
|
||||
location ^~ /xhs/humanSettings { try_files /index.html =200; }
|
||||
location ^~ /xhs/agentPair { try_files /index.html =200; }
|
||||
location = /xhs/analysis { try_files /index.html =200; }
|
||||
location = /xhs/analysis/ { try_files /index.html =200; }
|
||||
|
||||
location ^~ /wechat/register { try_files /index.html =200; }
|
||||
location ^~ /wechat/bind { try_files /index.html =200; }
|
||||
location ^~ /h5 { try_files /index.html =200; }
|
||||
location ^~ /track/query { try_files /index.html =200; }
|
||||
location ^~ /operation/inboundScan { try_files /index.html =200; }
|
||||
location ^~ /operation/outboundScan { try_files /index.html =200; }
|
||||
location ^~ /operation/printLabel { try_files /index.html =200; }
|
||||
location ^~ /operation/parcelArrival { try_files /index.html =200; }
|
||||
location ^~ /operation/fulfillPack { try_files /index.html =200; }
|
||||
|
||||
# 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先)
|
||||
location ^~ /order/priceCheck { try_files /index.html =200; }
|
||||
location ^~ /order/placeOrder { try_files /index.html =200; }
|
||||
location ^~ /order/myOrders { try_files /index.html =200; }
|
||||
location ^~ /order/fulfillOrders { try_files /index.html =200; }
|
||||
location ^~ /order/waybillManage { try_files /index.html =200; }
|
||||
location ^~ /order/pickupOrder { try_files /index.html =200; }
|
||||
location ^~ /order/customs9610Order { try_files /index.html =200; }
|
||||
location ^~ /order/batchJobs { try_files /index.html =200; }
|
||||
location ^~ /order/templateMapping { try_files /index.html =200; }
|
||||
location ^~ /order/platformStore { try_files /index.html =200; }
|
||||
location ^~ /order/platformStoreOrder { try_files /index.html =200; }
|
||||
|
||||
# 知识库前端路由(^~ 须比下方 /kb/ API 代理更优先)
|
||||
location ^~ /kb/category { try_files /index.html =200; }
|
||||
location ^~ /kb/article { try_files /index.html =200; }
|
||||
|
||||
location = /warehouse/parcel { try_files /index.html =200; }
|
||||
location = /warehouse/inventory { try_files /index.html =200; }
|
||||
location = /warehouse/sku { try_files /index.html =200; }
|
||||
location = /warehouse/binLocation { try_files /index.html =200; }
|
||||
location ^~ /warehouse/inboundNotice { try_files /index.html =200; }
|
||||
location ^~ /Plugin/ { try_files /index.html =200; }
|
||||
|
||||
location ^~ /channel/salesProducts { try_files /index.html =200; }
|
||||
location ^~ /channel/serviceChannels { try_files /index.html =200; }
|
||||
location ^~ /channel/priceArea { try_files /index.html =200; }
|
||||
location ^~ /channel/blacklist { try_files /index.html =200; }
|
||||
location ^~ /channel/remoteZones { try_files /index.html =200; }
|
||||
location ^~ /channel/weightRules { try_files /index.html =200; }
|
||||
location ^~ /channel/weightRounding { try_files /index.html =200; }
|
||||
location ^~ /channel/autoTrigger { try_files /index.html =200; }
|
||||
location ^~ /channel/issueTemplate { try_files /index.html =200; }
|
||||
location ^~ /channel/labelDesign { try_files /index.html =200; }
|
||||
|
||||
# 营销前端路由(^~ 须比下方 /marketing/ API 正则代理更优先)
|
||||
location ^~ /marketing/memberLevel { try_files /index.html =200; }
|
||||
location ^~ /marketing/pointsRule { try_files /index.html =200; }
|
||||
location ^~ /marketing/pointsFlow { try_files /index.html =200; }
|
||||
location ^~ /marketing/couponTemplate { try_files /index.html =200; }
|
||||
location ^~ /marketing/couponGrant { try_files /index.html =200; }
|
||||
location ^~ /marketing/commissionRule { try_files /index.html =200; }
|
||||
location ^~ /marketing/commissionDetail { try_files /index.html =200; }
|
||||
location ^~ /marketing/commissionWithdraw { try_files /index.html =200; }
|
||||
location ^~ /marketing/referralTree { try_files /index.html =200; }
|
||||
|
||||
# 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)
|
||||
location = /system/dict { try_files /index.html =200; }
|
||||
location = /system/warehouseAddress { try_files /index.html =200; }
|
||||
location = /system/announcement { try_files /index.html =200; }
|
||||
location = /system/country { try_files /index.html =200; }
|
||||
location ^~ /system/users { try_files /index.html =200; }
|
||||
location ^~ /system/roles { try_files /index.html =200; }
|
||||
location ^~ /system/menus { try_files /index.html =200; }
|
||||
location ^~ /system/organizations { try_files /index.html =200; }
|
||||
location ^~ /system/batchEmail { try_files /index.html =200; }
|
||||
location ^~ /system/otherConfig { try_files /index.html =200; }
|
||||
location ^~ /system/aiAssistant { try_files /index.html =200; }
|
||||
location ^~ /system/aiSkills { try_files /index.html =200; }
|
||||
location ^~ /system/mcpTools { try_files /index.html =200; }
|
||||
location ^~ /system/dataPerm/ { try_files /index.html =200; }
|
||||
|
||||
location ^~ /api/apiwechat/ {
|
||||
rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/api/ {
|
||||
rewrite ^/api/(.*)$ /itfx/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ^~ /Track {
|
||||
rewrite ^/Track(.*)$ /track/Track$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse|plugin|marketing|kb)/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location ^~ /wechat/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ^~ /operation/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ^~ /xhs/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location ^~ /uploads/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location ^~ /mcp/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location ^~ /ai/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host api.nanafa.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
location ~* \.(?:js|css)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
index index.html;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
||||
226
nginx-k3s.conf
226
nginx-k3s.conf
@ -1,226 +0,0 @@
|
||||
# k3s 方案 A:仅静态 SPA。API 由浏览器按域名直连(prod: api.ideship.com / test: api.test.ideship.com)。
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml image/svg+xml;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = / {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
|
||||
location = /9610.html {
|
||||
try_files /9610.html =404;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
location = /site {
|
||||
return 301 /;
|
||||
}
|
||||
location = /site/ {
|
||||
return 301 /;
|
||||
}
|
||||
location = /site/index.html {
|
||||
return 301 /;
|
||||
}
|
||||
|
||||
location ^~ /site/ {
|
||||
try_files $uri =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /favicon.ico {
|
||||
try_files /favicon.png =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /favicon.png {
|
||||
try_files /favicon.png =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /login {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
location = /login/ {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
|
||||
rewrite ^/admin/?$ /index.html last;
|
||||
|
||||
location = /admin {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
location = /admin/ {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
|
||||
location ^~ /xhs/startService { try_files $uri @spa; }
|
||||
location ^~ /xhs/creation { try_files $uri @spa; }
|
||||
location ^~ /xhs/publishHistory { try_files $uri @spa; }
|
||||
location ^~ /xhs/skills { try_files $uri @spa; }
|
||||
location ^~ /xhs/humanSettings { try_files $uri @spa; }
|
||||
location ^~ /xhs/agentPair { try_files $uri @spa; }
|
||||
location = /xhs/analysis { try_files $uri @spa; }
|
||||
location = /xhs/analysis/ { try_files $uri @spa; }
|
||||
|
||||
location ^~ /wechat/register { try_files $uri @spa; }
|
||||
location ^~ /wechat/bind { try_files $uri @spa; }
|
||||
location ^~ /h5 { try_files $uri @spa; }
|
||||
location ^~ /track/query { try_files $uri @spa; }
|
||||
location ^~ /operation/inboundScan { try_files $uri @spa; }
|
||||
location ^~ /operation/outboundScan { try_files $uri @spa; }
|
||||
location ^~ /operation/printLabel { try_files $uri @spa; }
|
||||
location ^~ /operation/parcelArrival { try_files $uri @spa; }
|
||||
location ^~ /operation/fulfillPack { try_files $uri @spa; }
|
||||
|
||||
# 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先)
|
||||
location ^~ /order/priceCheck { try_files $uri @spa; }
|
||||
location ^~ /order/placeOrder { try_files $uri @spa; }
|
||||
location ^~ /order/myOrders { try_files $uri @spa; }
|
||||
location ^~ /order/fulfillOrders { try_files $uri @spa; }
|
||||
location ^~ /order/waybillManage { try_files $uri @spa; }
|
||||
location ^~ /order/pickupOrder { try_files $uri @spa; }
|
||||
location ^~ /order/customs9610Order { try_files $uri @spa; }
|
||||
location ^~ /order/batchJobs { try_files $uri @spa; }
|
||||
location ^~ /order/templateMapping { try_files $uri @spa; }
|
||||
location ^~ /order/platformStore { try_files $uri @spa; }
|
||||
location ^~ /order/platformStoreOrder { try_files $uri @spa; }
|
||||
|
||||
# 知识库前端路由(^~ 须比下方 /kb/ API 代理更优先)
|
||||
location ^~ /kb/category { try_files $uri @spa; }
|
||||
location ^~ /kb/article { try_files $uri @spa; }
|
||||
|
||||
location = /warehouse/parcel { try_files $uri @spa; }
|
||||
location = /warehouse/inventory { try_files $uri @spa; }
|
||||
location = /warehouse/sku { try_files $uri @spa; }
|
||||
location = /warehouse/binLocation { try_files $uri @spa; }
|
||||
location ^~ /warehouse/inboundNotice { try_files $uri @spa; }
|
||||
location ^~ /Plugin/ { try_files $uri @spa; }
|
||||
|
||||
location ^~ /channel/salesProducts { try_files $uri @spa; }
|
||||
location ^~ /channel/serviceChannels { try_files $uri @spa; }
|
||||
location ^~ /channel/priceArea { try_files $uri @spa; }
|
||||
location ^~ /channel/blacklist { try_files $uri @spa; }
|
||||
location ^~ /channel/remoteZones { try_files $uri @spa; }
|
||||
location ^~ /channel/weightRules { try_files $uri @spa; }
|
||||
location ^~ /channel/weightRounding { try_files $uri @spa; }
|
||||
location ^~ /channel/autoTrigger { try_files $uri @spa; }
|
||||
location ^~ /channel/issueTemplate { try_files $uri @spa; }
|
||||
location ^~ /channel/labelDesign { try_files $uri @spa; }
|
||||
|
||||
# 官网线路目录。这条不是前端路由;落到下方 SPA 会返回 index.html,浏览器看不到 JSON。
|
||||
# 仅这一条转到对应环境的 Admin API(test → api.test.ideship.com,其余 → api.ideship.com)。
|
||||
location = /channel/sales-product/portal/catalog {
|
||||
set $oms_api_upstream api.ideship.com;
|
||||
if ($host ~* "(^test\.ideship\.com$|\.test\.ideship\.com$)") {
|
||||
set $oms_api_upstream api.test.ideship.com;
|
||||
}
|
||||
resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s;
|
||||
resolver_timeout 5s;
|
||||
proxy_http_version 1.1;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_set_header Host $oms_api_upstream;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass https://$oms_api_upstream/admin/channel/sales-product/portal/catalog$is_args$args;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# 营销前端路由(^~ 须比下方 /marketing/ API 正则代理更优先)
|
||||
location ^~ /marketing/memberLevel { try_files $uri @spa; }
|
||||
location ^~ /marketing/pointsRule { try_files $uri @spa; }
|
||||
location ^~ /marketing/pointsFlow { try_files $uri @spa; }
|
||||
location ^~ /marketing/couponTemplate { try_files $uri @spa; }
|
||||
location ^~ /marketing/couponGrant { try_files $uri @spa; }
|
||||
location ^~ /marketing/commissionRule { try_files $uri @spa; }
|
||||
location ^~ /marketing/commissionDetail { try_files $uri @spa; }
|
||||
location ^~ /marketing/commissionWithdraw { try_files $uri @spa; }
|
||||
location ^~ /marketing/referralTree { try_files $uri @spa; }
|
||||
|
||||
# 面单/截图同源代理:/__cos__/<host>/<path> → http://<host>/<path>
|
||||
# cos.* 自定义域 https 证书常仍是 *.myqcloud.com;上游继续走 http。
|
||||
# COS 签名 q-sign-time 含 ";":前端须编成 %3B,否则查询串被按 ";" 拆参 → Request has expired。
|
||||
location ~ ^/__cos__/([^/]+)(/.*)$ {
|
||||
set $cos_host $1;
|
||||
set $cos_uri $2;
|
||||
resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s;
|
||||
resolver_timeout 5s;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $cos_host;
|
||||
proxy_set_header Authorization "";
|
||||
proxy_set_header Cookie "";
|
||||
proxy_set_header X-Real-IP "";
|
||||
proxy_pass http://$cos_host$cos_uri$is_args$args;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_read_timeout 120s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)
|
||||
location = /system/dict { try_files $uri @spa; }
|
||||
location = /system/warehouseAddress { try_files $uri @spa; }
|
||||
location = /system/announcement { try_files $uri @spa; }
|
||||
location = /system/country { try_files $uri @spa; }
|
||||
location ^~ /system/users { try_files $uri @spa; }
|
||||
location ^~ /system/roles { try_files $uri @spa; }
|
||||
location ^~ /system/menus { try_files $uri @spa; }
|
||||
location ^~ /system/organizations { try_files $uri @spa; }
|
||||
location ^~ /system/batchEmail { try_files $uri @spa; }
|
||||
location ^~ /system/otherConfig { try_files $uri @spa; }
|
||||
location ^~ /system/aiAssistant { try_files $uri @spa; }
|
||||
location ^~ /system/aiSkills { try_files $uri @spa; }
|
||||
location ^~ /system/mcpTools { try_files $uri @spa; }
|
||||
location ^~ /system/dataPerm/ { try_files $uri @spa; }
|
||||
|
||||
# SPA 回退:try_files $uri @spa 统一带 no-cache,避免部署后浏览器缓存旧 index.html
|
||||
location @spa {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
# 微信 JS 安全域名校验:原样返回 txt,禁止 SPA 回落 index.html
|
||||
location ~* ^/MP_verify_[A-Za-z0-9]+\.txt$ {
|
||||
default_type text/plain;
|
||||
charset off;
|
||||
add_header Cache-Control "no-store";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~* \.(?:js|css)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
}
|
||||
@ -112,7 +112,7 @@ export async function chatAiAgent(payload: {
|
||||
toolKeys?: string[];
|
||||
maxToolRounds?: number;
|
||||
}): Promise<AiAgentChatResult> {
|
||||
// 走 /ai/agent/chat:线上已有;OMS nginx / Vite 须反代 /ai/(见 nginx-*.conf)
|
||||
// 走 /ai/agent/chat:开发 Vite 反代 /ai/;生产浏览器直连 api.* 域名
|
||||
// 后端另注册 /mcp/agent/chat,部署后可再切
|
||||
const res = await axiosInstance.post(mcpUrl('/ai/agent/chat'), payload);
|
||||
const x = assertBusinessOk(res.data) as any;
|
||||
|
||||
@ -106,8 +106,7 @@ export async function omsPost<T = any>(path: string, body?: Record<string, any>)
|
||||
|
||||
/**
|
||||
* COS 签名参数里的 `q-sign-time=start;end` 含分号。
|
||||
* 线上 nginx / 部分网关会把 `;` 当查询分隔符,拆成 `q-sign-time=start&end=…`,
|
||||
* COS 随即返回 AccessDenied / Request has expired;本地 Vite 代理通常不拆,故本地正常。
|
||||
* 部分代理会把 `;` 当查询分隔符拆坏签名;本地 Vite `/__cos__` 转发前编成 `%3B`。
|
||||
*/
|
||||
export function encodeCosSignedQuery(search: string): string {
|
||||
if (!search) return ''
|
||||
@ -115,10 +114,11 @@ export function encodeCosSignedQuery(search: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* COS 绝对地址 → 同源 `/__cos__/<host>/...`(开发 Vite / 生产 nginx 反代)。
|
||||
* cos.ideship.com 等自定义域名 https 不可用,直链 http 又会在 https 页面触发混合内容。
|
||||
* COS 绝对地址 → 同源 `/__cos__/<host>/...`(仅开发 Vite 反代)。
|
||||
* 生产已弃用站内 nginx,无 `/__cos__`;预览走 file-storage/stream,勿改写为该前缀(会落到 SPA)。
|
||||
*/
|
||||
function toCosSameOriginProxyUrl(absoluteUrl: string): string | null {
|
||||
if (!import.meta.env.DEV) return null
|
||||
try {
|
||||
const u = new URL(absoluteUrl)
|
||||
const host = u.hostname.toLowerCase()
|
||||
|
||||
@ -101,7 +101,7 @@ import {
|
||||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||
import PrintOptionsForm from '@/components/PrintTemplate/PrintOptionsForm.vue';
|
||||
import { fetchPrintAssetBytes, fetchPrintPreviewBlob } from '@/utils/printPreview';
|
||||
import { fetchPrintAssetBytes, openPrintAssetInNewTab } from '@/utils/printPreview';
|
||||
import {
|
||||
buildHiprintBlobPrintExtras,
|
||||
HIPRINT_PAPER_PRESET_OPTIONS,
|
||||
@ -258,34 +258,15 @@ function toAbsoluteUrl(url: string) {
|
||||
return `${window.location.origin}/${resolved}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看面单:经 file-storage/stream(或 /__cos__)拉字节再开 blob。
|
||||
* 勿把带 `q-sign-time=a;b` 的 COS 链直接丢给浏览器——线上反代/跳转常把 `;` 拆成 `&`,
|
||||
* COS 会回 AccessDenied/Request has expired(本地 Vite 不拆,故本地正常)。
|
||||
*/
|
||||
/** 查看面单:与附件预览同一套 stream → blob,避免直开 COS 证书/签名问题 */
|
||||
async function openItemInNative(item: PrintItem) {
|
||||
if (!item?.url) {
|
||||
Message.warning('当前无可打开的打印链接');
|
||||
return;
|
||||
}
|
||||
// 同步开空白页,避免 await 后被 Chrome 拦截弹窗
|
||||
const win = window.open('about:blank', '_blank');
|
||||
if (!win) {
|
||||
Message.warning('请允许浏览器弹窗以查看面单');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
win.document.title = '面单加载中…';
|
||||
const blob = await fetchPrintPreviewBlob(item.url);
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
win.location.replace(blobUrl);
|
||||
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
||||
await openPrintAssetInNewTab(item.url, { loadingTitle: '面单加载中…' });
|
||||
} catch (e: unknown) {
|
||||
try {
|
||||
win.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const msg = e instanceof Error ? e.message : String(e || '');
|
||||
Message.warning(msg || '面单打开失败');
|
||||
}
|
||||
|
||||
@ -47,14 +47,14 @@ function isCosAbsoluteUrl(raw: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 COS 绝对地址改写为同源 `/__cos__/<host>/...` 代理路径。
|
||||
* 开发由 Vite 代理;生产由 nginx-k3s `/__cos__/` 反代(避免浏览器直打 COS 撞 CORS)。
|
||||
* 把 COS 绝对地址改写为同源 `/__cos__/<host>/...`(仅开发 Vite)。
|
||||
* 生产无站内 nginx/`__cos__`,返回 null,由 file-storage/stream 拉取。
|
||||
*/
|
||||
export function toCosProxyUrl(url: string): string | null {
|
||||
if (!import.meta.env.DEV) return null
|
||||
try {
|
||||
const u = new URL(url)
|
||||
if (!isCosHost(u.host)) return null
|
||||
// 分号必须 %3B,否则线上反代会拆坏签名时间窗(见 encodeCosSignedQuery)
|
||||
return `${COS_PROXY_PREFIX}/${u.host}${u.pathname}${encodeCosSignedQuery(u.search)}`
|
||||
} catch {
|
||||
return null
|
||||
@ -307,8 +307,8 @@ async function fetchBlobViaSameOriginPath(path: string): Promise<Blob> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅经同源 `/__cos__` 代理拉取 COS(开发 Vite / 生产 nginx)。
|
||||
* 禁止浏览器直连 cos.*:私有桶未配 CORS 时必失败。
|
||||
* 仅经同源 `/__cos__` 代理拉取 COS(仅开发 Vite)。
|
||||
* 禁止浏览器直连 cos.*:私有桶未配 CORS / 自定义域证书常不匹配。
|
||||
*/
|
||||
async function fetchBlobViaCosProxy(rawUrl: string): Promise<Blob> {
|
||||
const proxied = toCosProxyUrl(rawUrl)
|
||||
@ -373,15 +373,6 @@ async function fetchPreviewBlob(rawUrl: string): Promise<Blob> {
|
||||
const streamed = await fetchViaFileStorageStream(raw, errors)
|
||||
if (streamed) return streamed
|
||||
|
||||
// 同源 /__cos__ 代理(生产 nginx;开发上面已试过则跳过)
|
||||
if (isCos && !import.meta.env.DEV) {
|
||||
try {
|
||||
return await fetchBlobViaCosProxy(raw)
|
||||
} catch (e: unknown) {
|
||||
errors.push(extractOmsRequestError(e) || 'COS 同源代理下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 同源 /uploads 路径
|
||||
const sameOriginPath = toSameOriginPath(raw)
|
||||
if (sameOriginPath.startsWith('/')) {
|
||||
@ -407,8 +398,6 @@ export async function fetchPrintPreviewBlob(rawUrl: string): Promise<Blob> {
|
||||
export function resolvePrintPreviewPdfSource(rawUrl: string): string | null {
|
||||
const raw = String(rawUrl || '').trim()
|
||||
if (!raw || !isPrintPreviewPdf(raw)) return null
|
||||
// 生产勿把 /__cos__ 交给前端直链:nginx 未配时会落到 SPA HTML
|
||||
if (!import.meta.env.DEV) return null
|
||||
return toCosProxyUrl(raw)
|
||||
}
|
||||
|
||||
@ -442,3 +431,75 @@ export async function fetchPrintAssetBytes(rawUrl: string): Promise<Uint8Array>
|
||||
}
|
||||
return new Uint8Array(await blob.arrayBuffer())
|
||||
}
|
||||
|
||||
/**
|
||||
* 新标签页预览面单/附件。
|
||||
* - 开发 + COS:同步打开 Vite `/__cos__/…`(`;`→`%3B`)。
|
||||
* - 生产:无站内反代,一律 file-storage/stream → blob(避免直开 cos 证书告警)。
|
||||
*/
|
||||
export async function openPrintAssetInNewTab(
|
||||
rawUrl: string,
|
||||
options?: { loadingTitle?: string },
|
||||
): Promise<void> {
|
||||
const raw = String(rawUrl || '').trim()
|
||||
if (!raw) {
|
||||
throw new Error('预览地址为空')
|
||||
}
|
||||
|
||||
// 仅 DEV 有 /__cos__;生产打开该路径会落到 SPA
|
||||
if (import.meta.env.DEV && /^https?:\/\//i.test(raw) && isCosAbsoluteUrl(raw)) {
|
||||
const proxied = toCosProxyUrl(raw)
|
||||
if (proxied) {
|
||||
const win = window.open(proxied, '_blank', 'noopener,noreferrer')
|
||||
if (!win) {
|
||||
throw new Error('请允许浏览器弹窗以预览文件')
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 同步开页,避免 await 后被当成非用户手势拦截;失败时在页内写错误,不直接 close
|
||||
const win = window.open('about:blank', '_blank')
|
||||
if (!win) {
|
||||
throw new Error('请允许浏览器弹窗以预览文件')
|
||||
}
|
||||
const title = options?.loadingTitle || '加载中…'
|
||||
try {
|
||||
win.document.open()
|
||||
win.document.write(
|
||||
`<!doctype html><title>${escapeHtml(title)}</title>` +
|
||||
`<body style="font:14px/1.5 sans-serif;padding:24px;color:#666">${escapeHtml(title)}…</body>`,
|
||||
)
|
||||
win.document.close()
|
||||
} catch {
|
||||
// 个别环境写 document 失败时仍继续拉 blob
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await fetchPreviewBlob(raw)
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
win.location.href = blobUrl
|
||||
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 120_000)
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e || '预览失败')
|
||||
try {
|
||||
win.document.open()
|
||||
win.document.write(
|
||||
`<!doctype html><title>预览失败</title>` +
|
||||
`<body style="font:14px/1.5 sans-serif;padding:24px;color:#c00">${escapeHtml(msg)}</body>`,
|
||||
)
|
||||
win.document.close()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
<a
|
||||
class="attach-file-name"
|
||||
href="javascript:;"
|
||||
@click.prevent="openFile(file)"
|
||||
@click.prevent="void openFile(file)"
|
||||
>
|
||||
{{ file.fileName || file.fileUrl }}
|
||||
</a>
|
||||
@ -53,7 +53,7 @@
|
||||
<a-button
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="openFile(file)"
|
||||
@click="void openFile(file)"
|
||||
>
|
||||
{{ $t('order.myOrders.drawer.attachments.preview') }}
|
||||
</a-button>
|
||||
@ -91,7 +91,8 @@ import {
|
||||
uploadOrderFiles,
|
||||
type OrderFileItem,
|
||||
} from '@/api/order';
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||
import { extractOmsRequestError } from '@/api/oms/common';
|
||||
import { openPrintAssetInNewTab } from '@/utils/printPreview';
|
||||
|
||||
const FILE_TYPE_DEFS = [
|
||||
{
|
||||
@ -166,26 +167,24 @@ const visibleSections = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
/** 与打印弹窗「查看面单」一致:同步打开可访问 URL(勿先 await 再开 blob,Chrome 会当搜索词) */
|
||||
function toAbsoluteUrl(path?: string) {
|
||||
const resolved = resolveOmsAssetUrl(path) || String(path || '').trim();
|
||||
if (!resolved) return '';
|
||||
if (/^https?:\/\//i.test(resolved) || resolved.startsWith('blob:') || resolved.startsWith('data:')) {
|
||||
return resolved;
|
||||
}
|
||||
if (resolved.startsWith('/')) return `${window.location.origin}${resolved}`;
|
||||
return `${window.location.origin}/${resolved}`;
|
||||
}
|
||||
|
||||
function openFile(file: OrderFileItem) {
|
||||
const url = toAbsoluteUrl(file.fileUrl);
|
||||
if (!url) {
|
||||
/** 与打印弹窗「查看面单」一致:开发可走 /__cos__,生产 stream → blob,不直开 cos 域名 */
|
||||
async function openFile(file: OrderFileItem) {
|
||||
const raw = String(file?.fileUrl || '').trim();
|
||||
if (!raw) {
|
||||
Message.warning(t('order.myOrders.drawer.attachments.previewFailed'));
|
||||
return;
|
||||
}
|
||||
const win = window.open(url, '_blank', 'noopener,noreferrer');
|
||||
if (!win) {
|
||||
Message.warning(t('order.myOrders.drawer.attachments.previewBlocked'));
|
||||
try {
|
||||
await openPrintAssetInNewTab(raw, {
|
||||
loadingTitle: t('order.myOrders.drawer.attachments.preview'),
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
const msg = extractOmsRequestError(e) || (e instanceof Error ? e.message : '');
|
||||
if (/弹窗|popup|blocked/i.test(msg)) {
|
||||
Message.warning(t('order.myOrders.drawer.attachments.previewBlocked'));
|
||||
return;
|
||||
}
|
||||
Message.warning(msg || t('order.myOrders.drawer.attachments.previewFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -276,7 +276,7 @@ export default defineConfig(({ mode }) => {
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
// 开发环境面单/截图:同源代理 COS(自定义域名 https 不可用,上游走 http)
|
||||
// 仅开发:同源代理 COS(生产无 /__cos__,走 file-storage/stream)
|
||||
'/__cos__': {
|
||||
target: 'http://cos.ideship.com',
|
||||
changeOrigin: true,
|
||||
@ -382,7 +382,7 @@ export default defineConfig(({ mode }) => {
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
// MCP 工具 / AI Agent(须与 nginx /mcp/ 反代一致)
|
||||
// MCP 工具 / AI Agent(开发 Vite 反代;生产直连 api.*)
|
||||
'/mcp': {
|
||||
target: omsAdminTarget,
|
||||
changeOrigin: true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user