部署与服务器配置
QXJ 当前实际维护两套互相独立的部署环境,对应鸿蒙安全浏览器出厂预置的两条 服务器配置:
- 环境① 外网测试服务器(
117.72.72.201):没有 Nginx,认证后端与业务后端 各自以 gunicorn 直接终结 TLS 的方式启动,使用自签证书,并且两个进程都是 「后端带前端」同源托管 SPA; - 环境② 阿里云域名测试服务器(
8.140.216.214):宝塔 Nginx 反代一层, 认证后端、业务后端、鉴权前端同机,证书为宝塔免费 DV 证书;业务前端独立 托管在 GitHub Pages;应用商店也部署在这台机器上。
本页说明两套环境的拓扑、服务端配置、自签证书在客户端的处理路径,以及线上 站点的实际访问情况。
部署总览
两套环境对比:
| 项 | 环境① 外网测试服 | 环境② 阿里云域名服 |
|---|---|---|
| 公网地址 | 117.72.72.201 | 8.140.216.214 |
| 接入方式 | IP + 端口直连(4607 / 8080) | 域名 443(*.qxj.nsp.ac.cn、apps.nsp.ac.cn) |
| 反向代理 | 无 | 宝塔 Nginx 一层 |
| TLS 终结 | gunicorn --certfile / --keyfile | Nginx ssl_certificate |
| 证书 | 自签(certs/server.crt,SAN 含 IP) | 宝塔免费 DV 证书 |
| 认证前后端 | gunicorn :4607,后端带前端同源 | qxj-backend-admin.qxj.nsp.ac.cn → 127.0.0.1:4607 |
| 业务后端 | gunicorn :8080,后端带前端同源 | qxj-backend-mock.qxj.nsp.ac.cn → 127.0.0.1:8080 |
| 业务前端 | :8080 同源 SPA | GitHub Pages(跨域,JSBridge 取凭据) |
| 应用商店 | — | 同机 Nginx:https://apps.nsp.ac.cn/appstore |
TRUSTED_PROXY_COUNT | 0(进程自己说 TLS) | 1(信任单层 Nginx 的 X-Forwarded-Proto) |
| 终端预置配置 | JD_Server_HTTPS(默认选中) | Ali_Server_HTTPS |
要点:
- 两套环境各自独立运行、各自带 Redis,不是主备关系,分别用于自签直连联调 与域名 HTTPS 对外演示;
- 业务前端可以不与后端同源——Ali_Server_HTTPS 把业务前端指向 GitHub Pages, 借助 JSBridge 跨域获取凭据,避免在前端 localStorage 存放 token;
- 应用商店是纯静态站点,经标准 HTTPS(443)访问,安装包实体托管在腾讯云 COS; 早期的
:7777端口入口已废弃。
终端预置服务器配置
配置模型
终端的服务器配置由 [ServerConfig.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/common/src/main/ets/model/ServerConfig.ets) 定义,每条配置携带 4 个 URL 与扫码绑定态:
// common/src/main/ets/model/ServerConfig.ets
export class ServerConfig {
name: string;
backendUrl: string; // 鉴权后端(SM2 握手 / 登录 / token)
frontendUrl: string; // 鉴权前端(管理后台,可选)
businessBackendUrl: string; // 业务后端(接入方)
businessFrontendUrl: string; // 业务前端(接入方页面,浏览器默认打开)
serverId: string; // 服务器设备 ID(扫码绑定后填充)
serverPubKey: string; // 服务器 SM2 公钥(扫码绑定后填充,128 hex)
}2
3
4
5
6
7
8
9
10
- 配置列表通过
PersistentStorage持久化(键SERVER_CONFIG_LIST), 当前选中索引持久化在CURRENT_SERVER_INDEX; - 持久化反序列化的对象会丢失类方法,因此判定是否已扫码绑定用模块级函数
isServerBound(c)(真值判断,兼容旧数据缺字段为undefined的情况), 不能调用c.isBound(); - 持久化注册必须在 UI 实例初始化成功后调用
initServerConfigPersistentStorage()(幂等),过早调用会导致持久化失败。
两套默认预置配置
LISTS_SAVED 是出厂随包的两套配置,DEFAULT_SERVER 指向 JD_Server_HTTPS (自签证书直连 IP+端口)。其余历史配置(台式机 HTTP/HTTPS、ThinkBook 热点、 气象局域名、宽松配置)已注释,仅用于内部调试,不入包。
// common/src/main/ets/model/ServerConfig.ets
/** JD_Server_HTTPS(严格) —— 自签证书直连 IP+端口,后端带前端同源 */
export const JD_Server_HTTPS: ServerConfig = new ServerConfig(
'外网测试服务器HTTPS(严格)',
'https://117.72.72.201:4607', // 鉴权后端
'https://117.72.72.201:4607', // 鉴权前端(同后端,SPA 同源托管)
'https://117.72.72.201:8080', // 业务后端
'https://117.72.72.201:8080', // 业务前端(同业务后端,SPA 同源托管)
);
/** Ali_Server_HTTPS(严格) —— 域名 HTTPS + GitHub Pages 业务前端 */
export const Ali_Server_HTTPS: ServerConfig = new ServerConfig(
'阿里云域名测试服务器HTTPS(严格)',
'https://qxj-backend-admin.qxj.nsp.ac.cn', // 鉴权后端
'https://qxj-backend-admin.qxj.nsp.ac.cn', // 鉴权前端
'https://qxj-backend-mock.qxj.nsp.ac.cn', // 业务后端
'https://etsuyou.github.io/qxj-frontend-mock', // 业务前端(GitHub Pages)
);
/** 默认服务器 */
export const DEFAULT_SERVER: ServerConfig = JD_Server_HTTPS;
/** 预置服务器列表 */
export const LISTS_SAVED: ServerConfig[] = [
JD_Server_HTTPS,
Ali_Server_HTTPS
];2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| 配置 | 鉴权后端 | 业务后端 | 业务前端 | 证书 | 适用场景 |
|---|---|---|---|---|---|
JD_Server_HTTPS(默认) | 117.72.72.201:4607 | 117.72.72.201:8080 | 同业务后端 | 自签(gunicorn 直接 TLS,无 Nginx) | 联调 / 自签演示,客户端需用户知情跳过校验 |
Ali_Server_HTTPS | qxj-backend-admin.qxj.nsp.ac.cn | qxj-backend-mock.qxj.nsp.ac.cn | etsuyou.github.io | 宝塔免费 DV(Nginx 终止) | 对外演示,业务前端走 GitHub Pages |
「严格」指 fail-closed 模式:证书校验失败默认拒绝,需用户在弹窗中主动确认 才跳过;与之对照的历史
*_UNSAFE配置(宽松,端口前缀1,如14607/18080) 已注释,不再随包。
自签证书的客户端处理
JD_Server_HTTPS 使用自签证书,HarmonyOS @ohos.net.http 默认会以错误码 2300060(CURLcode 60 的 NetStack 基址)拒绝连接。客户端不会静默放行,而是 识别该错误后弹窗告知风险,由用户知情后选择是否跳过。
错误识别(HttpUtil.ets)
[HttpUtil.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/common/src/main/ets/util/HttpUtil.ets) 定义了专用的 CertValidationError 与判定函数,供密钥协商、加密登录上层统一捕获:
// common/src/main/ets/util/HttpUtil.ets
/** SSL 证书校验失败错误码(CURLcode 60,NetStack 错误基址 2300000) */
export const SSL_CERT_ERROR_CODE: number = 2300060;
/** remoteValidation 字段最低支持版本(API 18 起可跳过证书校验) */
const MIN_API_REMOTE_VALIDATION: number = 18;
export class CertValidationError extends Error {
certErrorCode: number;
constructor(message: string, certErrorCode: number) {
super(message);
this.name = 'CertValidationError';
this.certErrorCode = certErrorCode;
}
}
/** 判断异常是否为 SSL 证书校验失败(按错误码,消息兜底) */
function isCertValidationError(e: BusinessError): boolean {
if (e.code === SSL_CERT_ERROR_CODE) {
return true;
}
const msg: string = e.message || '';
return msg.includes('SSL peer certificate') || msg.includes('SSH remote key');
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
跳过校验的调用路径
postRawFrame 暴露 skipCertValidation 参数,仅在 API ≥ 18 时设置 remoteValidation = 'skip';低版本不设置该字段(避免运行时报错),直接提示 无法跳过:
// common/src/main/ets/util/HttpUtil.ets
static async postRawFrame(
baseUrl: string, path: string, frameHex: string,
skipCertValidation: boolean = false,
): Promise<string> {
// ... 构造 options ...
// 用户知情确认后跳过证书校验(自签名场景);API<18 不支持该字段,不设置
if (skipCertValidation && deviceInfo.sdkApiVersion >= MIN_API_REMOTE_VALIDATION) {
options.remoteValidation = 'skip';
}
const response = await httpReq.request(url, options);
// ...
} catch (err) {
const e: BusinessError = err as BusinessError;
if (isCertValidationError(e)) {
throw new CertValidationError(
`服务器证书不受信任: ${e.message}`, e.code);
}
// ...
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
设计要点:
- fail-closed:默认不跳过,只有上层(弹窗回调)显式传
true才跳过; - 版本门控:
remoteValidation是 API 18+ 字段,低版本强行设置会抛异常, 因此用deviceInfo.sdkApiVersion做版本守卫; - 错误归类:把 SSL 错误单独包成
CertValidationError,不与普通网络错误混淆, 便于上层按类型弹窗而非当成可重试的瞬时故障。
业务前端默认加载
浏览器启动时若没有待加载的 URL,会默认打开当前选中服务器的 businessFrontendUrl(而非空白新标签页),使设备一开机即进入业务页面:
// features/browser/src/main/ets/view/BrowserView.ets
// 若从历史记录页带 URL 跳转回来,立即加载
if (this.pendingUrl.length > 0) {
this.onPendingUrl();
} else {
// 默认打开当前选中服务器的业务前端(不再显示空白新标签页)
const server = DeviceStatusUtil.getCurrentServer();
if (server !== null && server.businessFrontendUrl.length > 0) {
this.url = server.businessFrontendUrl;
this.inputUrl = server.businessFrontendUrl;
this.isLoaded = true;
Logger.info(TAG, `默认加载业务前端: ${this.url}`);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
- 切换服务器时,
refreshServerDisplay()同步刷新首页服务器展示信息与 JSBridge 应用侧 origin 白名单——白名单取当前服务器的四个 URL, 切服务器即时生效,fail-closed; - 因此 JD_Server_HTTPS 下业务前端在
117.72.72.201:8080,Ali_Server_HTTPS 下 业务前端跳到etsuyou.github.io,JSBridge 在两种 origin 下都能注入。
服务端部署(环境①):gunicorn 直接 TLS
外网测试服 117.72.72.201 不安装 Nginx,gunicorn 原生支持 TLS (--certfile / --keyfile),直接以 HTTPS 对公网提供服务。证书复用工程 certs/ 目录下的自签证书(qxj-backend-admin/certs/server.crt + server.key, SAN 必须包含访问用 IP,可用 python -m tools.setup gen-cert 117.72.72.201 重新生成)。
完整步骤见后端文档 docs/生产环境HTTPS部署-gunicorn.md,认证后端的常驻启动命令:
# 认证后端 :4607(生产配置 + 自签证书 + 管理前端 frontend_dist 同源)
DJANGO_SETTINGS_MODULE=config.settings.production \
nohup gunicorn config.wsgi:application \
--bind 0.0.0.0:4607 \
--certfile certs/server.crt \
--keyfile certs/server.key \
--workers 3 --timeout 120 \
--pid logs/gunicorn-4607.pid \
--access-logfile logs/access-4607.log \
--error-logfile logs/error-4607.log \
> logs/gunicorn-4607.log 2>&1 &2
3
4
5
6
7
8
9
10
11
业务后端 :8080(qxj-backend-mock-demo 的 after 工程)以完全相同的形态启动 第二个 gunicorn:换成 --bind 0.0.0.0:8080、该工程自己的 wsgi 模块与同目录 自签证书即可。两个进程各自同源输出前端页面(认证侧管理端构建产物在 frontend_dist/,由 WhiteNoise 提供并为 Vue history 路由做 index.html 回退), 共享本机 Redis。
gunicorn 直接 TLS 模式的关键约束:
.env.production中TRUSTED_PROXY_COUNT=0——没有代理,Django 自己就能 识别 HTTPS;- 不提供 80 → 443 跳转:同端口只说 TLS,明文 HTTP 请求在握手阶段即被断开, 需要跳转时只能在前面再加一层 Nginx;
- 自签证书的 SAN 必须包含
117.72.72.201,否则客户端报「证书地址不匹配」; - 生产配置下 HTTPS 加固无条件生效(SSL 重定向 / HSTS / 安全 Cookie),无旁路开关。
服务端部署(环境②):宝塔 Nginx 反向代理
阿里云域名服 8.140.216.214 由宝塔面板统一管理 Nginx:三个站点 (认证后端、业务后端、应用商店)都监听 443,TLS 在 Nginx 处终止,后端进程 改为以普通 HTTP 绑 127.0.0.1,由 Nginx 反代一层。证书在宝塔站点设置中 申请与续期,使用其免费 DV 证书(证书与私钥由宝塔按站点托管、自动续期)。
后端站点:反代到本机 gunicorn
认证站点 qxj-backend-admin.qxj.nsp.ac.cn 的配置形态(业务站点结构相同, 换 server_name 与 proxy_pass 目标端口):
# 认证后端站点(形态示意)
server {
listen 443 ssl http2;
server_name qxj-backend-admin.qxj.nsp.ac.cn;
# 宝塔免费 DV 证书(路径由宝塔按站点管理)
ssl_certificate /www/server/panel/vhost/cert/qxj-backend-admin.qxj.nsp.ac.cn/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/qxj-backend-admin.qxj.nsp.ac.cn/privkey.pem;
location / {
# 反代到本机 gunicorn(明文 HTTP)
proxy_pass http://127.0.0.1:4607;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Django 据此头识别原始协议为 HTTPS
proxy_set_header X-Forwarded-Proto $scheme;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
配套要求:
- 后端
.env.production设TRUSTED_PROXY_COUNT=1,Django 才信任单层 Nginx 的X-Forwarded-Proto: https(直 TLS 模式则保持0); ALLOWED_HOSTS必须包含三个域名(漏配返回 400 DisallowedHost),CORS_ALLOWED_ORIGINS包含 GitHub Pages 业务前端来源;- 管理前端仍由后端
frontend_dist/(WhiteNoise)同源输出,Nginx 只做透传; 也可在 Nginx 侧直接托管静态产物以获得缓存优势。
应用商店站点:静态站 + SPA 兜底
应用商店是纯静态站点,与两个后端站点并列托管在同一台 Nginx 上:
# apps.nsp.ac.cn 站点(形态示意,敏感文件拦截规则沿用仓库内历史配置)
server {
listen 443 ssl http2;
server_name apps.nsp.ac.cn;
root /www/wwwroot/apps.nsp.ac.cn;
index index.html;
ssl_certificate /www/server/panel/vhost/cert/apps.nsp.ac.cn/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/apps.nsp.ac.cn/privkey.pem;
location / {
# SPA 路由兜底:/appstore 是前端路由路径,不是子目录部署
try_files $uri $uri/ /index.html;
}
# 禁止访问的敏感文件(.env / package.json / Dockerfile / 各语言锁文件等)
location ~* (\.env.*|\.git.*|package(-lock)?\.json|pnpm-lock\.yaml|\
Dockerfile|docker-compose\.yml|pyproject\.toml|requirements\.txt) {
return 404;
}
# 禁止访问的敏感目录
location ~* /(\.git|\.svn|\.idea|\.ssh|node_modules|runtime)/ {
return 404;
}
# 证书验证目录放行,但禁止在验证目录放可执行/打包文件
location ~ \.well-known { allow all; }
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|zip|tar\.gz|sql|bak)$" ) {
return 403;
}
# 静态资源缓存
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { expires 30d; }
location ~ .*\.(js|css)?$ { expires 12h; }
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
宝塔站点配置要点:
| 项 | 说明 |
|---|---|
| 监听 | 三个站点统一 443 ssl http2(80 由宝塔做 301 跳转) |
server_name | apps.nsp.ac.cn、qxj-backend-admin.qxj.nsp.ac.cn、qxj-backend-mock.qxj.nsp.ac.cn |
| SPA 兜底 | try_files $uri $uri/ /index.html——/appstore 是前端路由路径而非子目录部署 |
| 敏感文件拦截 | 正则匹配 .env / .git* / 各语言锁文件 / Dockerfile 等返回 404,防止源码与配置泄露 |
| 证书 | 宝塔站点内一键申请的免费 DV 证书,自动托管与续期 |
| 缓存策略 | 图片 30 天、JS/CSS 12 小时,降低回源 |
应用商店仓库内的 [nginx.conf](file:///c:/Users/etsuyou/Desktop/qxj-project/apps.nsp.ac.cn/nginx.conf) 是早期部署在外网测试服时的历史配置(
listen 7777+ IP 直连),该入口已停用; 现网以上面的 443 域名站点为准。
线上站点一览
| 站点 | 地址 | 对应仓库 | 实测可用 | 证书 / 形态 |
|---|---|---|---|---|
| NSP 应用商店 | https://apps.nsp.ac.cn/appstore | apps.nsp.ac.cn | ✅ 可访问 | 宝塔免费 DV,阿里云服 Nginx 静态站 |
| 认证 / 管理后端 | https://qxj-backend-admin.qxj.nsp.ac.cn | qxj-backend-admin | ✅ 可访问(Ali 配置) | 宝塔免费 DV,Nginx → gunicorn |
| Mock 业务后端 | https://qxj-backend-mock.qxj.nsp.ac.cn | qxj-backend-mock-demo | ✅ 可访问(Ali 配置) | 宝塔免费 DV,Nginx → gunicorn |
| 外网测试(默认) | https://117.72.72.201:4607 | qxj-backend-admin | ⚠️ 自签证书,公开浏览器拦截,终端用户确认后放行 | 自签,gunicorn 直接 TLS(无 Nginx) |
| 外网业务后端 | https://117.72.72.201:8080 | qxj-backend-mock-demo | ⚠️ 同上 | 自签,gunicorn 直接 TLS(无 Nginx) |
| 业务前端 Mock | https://etsuyou.github.io/qxj-frontend-mock/ | qxj-frontend-mock | ✅ 可访问 | GitHub Pages HTTPS |
应用商店(apps.nsp.ac.cn)
仿 Apple App Store 风格的纯静态站点,左侧深色侧栏(搜索 + 应用中心 / 专题), 主区「APP OF THE DAY」焦点卡为风四C星浏览器,下方「今日焦点」横排展示 晨读 / 简截 / 色卡 / 番茄钟等占位应用。详情页提供截图预览、版本历史与安装包 下载入口,安装包实体托管在腾讯云 COS。全站经标准 HTTPS 访问,无专用端口。
业务前端 Mock(GitHub Pages)
标题「QXJ Demo · 改造后(JSBridge + 国密方案)」,演示改造后的业务接入 方案:不在 localStorage 存 token,通过 NSP-SM(SM2 签名 + SM3-HMAC)+ 鸿蒙 Pad 原生 JSBridge 完成加密登录,页面提供「跳转到登录页」按钮与本地存储状态 检查。这是 Ali_Server_HTTPS 配置下浏览器默认加载的业务前端。
外网测试服务器(117.72.72.201)
JD_Server_HTTPS 的目标,认证后端 :4607、业务后端 :8080,两个 gunicorn 进程各自带自签证书直接跑 HTTPS,均无 Nginx。公开浏览器直接访问会被 ERR_CERT_AUTHORITY_INVALID 拦截——这是设计预期:终端通过 HttpUtil 识别 2300060 后弹窗让用户知情跳过,而非给公开浏览器直接消费。
端口与域名约定
| 端口 / 域名 | 用途 | 归属 |
|---|---|---|
443 | 阿里云服 Nginx:应用商店 / 认证后端 / 业务后端三站点(TLS 终止 + 反代) | 8.140.216.214 |
4607 | 认证后端 gunicorn:外网服以 TLS 直连公网;阿里云服绑 127.0.0.1 经 Nginx 反代 | qxj-backend-admin |
8080 | 业务后端(mock after):两种环境的对外形态同 4607 | qxj-backend-mock-demo |
3006 | 鉴权前端(管理端)开发端口 | qxj-frontend-admin |
5173 / 5174 | Vite 开发服务器(业务前端 / env 校验 Web) | mock-demo / env-helper-web |
8.140.216.214 | 阿里云域名服公网 IP(Ali 配置,应用商店同机) | 对外演示 |
117.72.72.201 | 外网测试服公网 IP(JD 配置,无 Nginx) | 联调 / 自签演示 |
*.qxj.nsp.ac.cn | 阿里云域名(admin / mock) | 对外演示 |
etsuyou.github.io | GitHub Pages(业务前端 Mock) | 对外演示 |
生产
14607/13006/18080等带1前缀的端口为历史宽松配置(HTTP), 已注释,不再随包;应用商店早期的7777端口入口同样已废弃,统一走 https://apps.nsp.ac.cn/appstore。端口口径与 README 不一致时,以本表为排查基准。
部署流程
环境① 外网测试服(gunicorn 直接 TLS)
# 1. 前端构建产物同源托管
cd ../qxj-frontend-admin && pnpm build:backend # 输出到 qxj-backend-admin/frontend_dist/
# 2. 后端依赖与初始化(首次)
cd ../qxj-backend-admin
pip install -r requirements.txt
python tools/setup.py init # 初始化 SM2 密钥与超管等
# 3. 确认/生成自签证书(SAN 必须含公网 IP)
python -m tools.setup gen-cert 117.72.72.201
# 4. 以生产配置直接 TLS 启动(.env.production:TRUSTED_PROXY_COUNT=0)
DJANGO_SETTINGS_MODULE=config.settings.production \
gunicorn config.wsgi:application -b 0.0.0.0:4607 \
--certfile certs/server.crt --keyfile certs/server.key \
--workers 3 --timeout 120
# 5. 业务后端 :8080 同形态启动第二个 gunicorn(换 wsgi 模块与端口)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
环境② 阿里云域名服(宝塔 Nginx 反代)
# 1. gunicorn 以普通 HTTP 绑回环地址(不再传 --certfile/--keyfile)
gunicorn config.wsgi:application -b 127.0.0.1:4607 \
--workers 4 --threads 8 --timeout 60
# 业务后端同理绑 127.0.0.1:8080
# 2. 宝塔面板新建三个站点并申请免费 DV 证书:
# apps.nsp.ac.cn(静态站)/ qxj-backend-admin / qxj-backend-mock(反向代理)
# 3. .env.production 配套调整
# TRUSTED_PROXY_COUNT=1(信任单层 Nginx)
# ALLOWED_HOSTS 与 CORS_ALLOWED_ORIGINS 含全部域名与 GitHub Pages 来源2
3
4
5
6
7
8
9
10
11
应用商店发布
# 1. 本地构建(vue-tsc 类型检查 + vite build)
pnpm install
pnpm build # 输出 dist/
# 2. 发布新版本:把 COS 控制台复制的发布文本粘到 tmp/paste.txt
pnpm add-version # 正式版,写入 src/data/apps.ts 的 QXJ_VERSIONS
pnpm add-dev-version # 尝鲜版,写入 QXJ_DEV_VERSIONS
# 3. 重新构建并上传 dist/ 到阿里云服宝塔站点根目录
pnpm build
# 通过宝塔文件管理或 scp 上传到 apps.nsp.ac.cn 站点根目录(443 站点)2
3
4
5
6
7
8
9
10
11