优化小程序部分

This commit is contained in:
hero920103 committed 2026-10-31 19:17:37 +08:00
1 parent 727319a6eb
commit d4e89788c5
46 files changed
+4906 -63

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
# ============================================================
# 宝塔站点 api.yunzer.cn 的 nginx 配置(留档副本)
# 文件位置(服务器):/www/server/panel/vhost/nginx/api.yunzer.cn.conf
# 面板入口:宝塔 → 网站 → api.yunzer.cn → 设置 → 配置文件
#
# 【2026-10-31 新增】根路径域名校验文件的 http 放行
# 微信公众平台 / 小程序配置「业务域名」时,校验服务是走 **http**
# 去抓取根目录下的校验文件(如 /1XXl8fPHr9.txt)。
# 原配置把 http 一律 301 到 https,微信拿到重定向就判
# 「校验文件验证失败,请确认已在域名根目录正确放置校验文件」。
# 下面 HTTP_TO_HTTPS 块里新增的 if 例外,就是让根路径下的 .txt
# 校验文件在 http 下直接返回文件内容(不跳转)。
# 用通配而不是写死文件名:微信每次重新下载校验文件都会生成新随机名。
#
# 【注意】不要设 verify_hosts
# 本文件里 proxy_set_header Host 10.31.100.2; 把 Host 头改成了 IP,
# Go 侧看不到 api.yunzer.cn 这个域名。verify_hosts 必须留空
# (留空 = 所有走本服务的域名都放行)。
#
# 【注意】location ^~ / 会让后面的正则 location 失效
# ^~ 匹配后 nginx 不再检查正则 location,所以下面两条
# 「禁止访问的敏感文件 / 敏感目录」的 location ~* 规则在本站点实际不生效。
# 当前无泄露风险(Go 不提供工作目录文件,未知路径一律 404),但要知道这层保护是空的。
#
# 【注意】proxy_cache_path 定义了缓存区但当前未启用 proxy_cache
# 以后若启用全局缓存,301 会被缓存住,微信可能拿到缓存的旧响应,届时需清缓存。
# ============================================================
proxy_cache_path /www/wwwroot/api.yunzer.cn/proxy_cache_dir levels=1:2 keys_zone=api_yunzer_cn_cache:20m inactive=1d max_size=5g;
server {
listen 80;
listen 443 ssl;
http2 on;
listen [::]:80;
server_name api.yunzer.cn;
index index.php index.html index.htm default.php default.htm default.html;
root /www/wwwroot/api.yunzer.cn;
include /www/server/panel/vhost/nginx/extension/api.yunzer.cn/*.conf;
#CERT-APPLY-CHECK--START
# 用于SSL证书申请时的文件验证相关配置 -- 请勿删除
include /www/server/panel/vhost/nginx/well-known/api.yunzer.cn.conf;
#CERT-APPLY-CHECK--END
#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
#error_page 404/404.html;
#HTTP_TO_HTTPS_START
set $isRedcert 1;
if ($server_port != 443) {
set $isRedcert 2;
}
if ( $uri ~ /\.well-known/ ) {
set $isRedcert 1;
}
# 域名校验文件(微信/搜索引擎/第三方平台)必须能用 http 直接访问,不能 301 跳 https。
# 用 [.] 代替 \. ,避免 nginx 配置解析里反斜杠转义的歧义。
if ( $uri ~ ^/[A-Za-z0-9._-]+[.]txt$ ) {
set $isRedcert 1;
}
if ($isRedcert != 1) {
rewrite ^(/.*)$ https://$host$1 permanent;
}
#HTTP_TO_HTTPS_END
ssl_certificate /www/server/panel/vhost/cert/api.yunzer.cn/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/api.yunzer.cn/privkey.pem;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_tickets on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000";
error_page 497 https://$host$request_uri;
#SSL-END
#REDIRECT START
#REDIRECT END
#ERROR-PAGE-START 错误页配置,可以注释、删除或修改
#error_page 404 /404.html;
#error_page 502 /502.html;
#ERROR-PAGE-END
#PHP-INFO-START PHP引用配置,可以注释或修改
include enable-php-00.conf;
#PHP-INFO-END
#IP-RESTRICT-START 限制访问ip的配置,IP黑白名单
#IP-RESTRICT-END
#BASICAUTH START
#BASICAUTH END
#SUB_FILTER START
#SUB_FILTER END
#GZIP START
#GZIP END
#GLOBAL-CACHE START
#GLOBAL-CACHE END
#WEBSOCKET-SUPPORT START
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
#WEBSOCKET-SUPPORT END
#PROXY-CONF-START
location ^~ / {
proxy_pass http://10.31.100.2:9000;
proxy_set_header Host 10.31.100.2;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header REMOTE-HOST $remote_addr;
# 连接超时(建立连接的时间)
proxy_connect_timeout 3600s;
# 发送超时(发送请求到后端的时间)
proxy_send_timeout 3600s;
# 读取超时(等待后端响应的时间)
proxy_read_timeout 3600s;
# 支持websocket链接
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# 客户端请求体大小限制(0 表示不限制)
client_max_body_size 0;
# 客户端请求体超时(客户端发送请求体的时间)
client_body_timeout 3600s;
# 客户端请求体缓冲区大小
client_body_buffer_size 128k;
# 禁用请求体缓冲(直接流式传输到后端,不先缓存到磁盘)
proxy_request_buffering off;
# 禁用响应缓冲(直接流式传输到客户端)
proxy_buffering off;
}
#PROXY-CONF-END
#SERVER-BLOCK START
#SERVER-BLOCK END
# 禁止访问的敏感文件
location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$ {
return 404;
}
# 禁止访问的敏感目录
location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ {
return 404;
}
#一键申请SSL证书验证目录相关设置
location /.well-known {
allow all;
}
#禁止在证书验证目录放入敏感文件
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
return 403;
}
#LOG START
access_log /www/wwwlogs/api.yunzer.cn.log;
error_log /www/wwwlogs/api.yunzer.cn.error.log;
#LOG END
}
@@ -0,0 +1,21 @@
-- 微信小程序多应用表补充「消息推送」配置字段
--
-- 对应小程序后台:开发管理 → 开发设置 → 消息推送
-- Token / EncodingAESKey / 消息加解密方式 / 数据格式
-- 平台端「小程序配置 - 编辑小程序 - 消息推送」维护,保存后把「回调地址」填到后台。
--
-- 注意:aes_key 以 AES-256-GCM 加密存储(复用 wechatmp 的密钥),与 AppSecret 同一套加密。
-- 执行:mysql -h <host> -P <port> -u <user> -p <db> < alter_wechat_mini_app_push.sql
-- ============================================================
ALTER TABLE `yz_platform_wechat_mini_app`
ADD COLUMN `token` varchar(128) NOT NULL DEFAULT '' COMMENT '消息推送 Token(小程序后台「消息推送」填写)' AFTER `qrcode_url`,
ADD COLUMN `aes_key` varchar(512) NOT NULL DEFAULT '' COMMENT '消息推送 EncodingAESKey(AES-256-GCM 加密存储)' AFTER `token`,
ADD COLUMN `encrypt_mode` varchar(16) NOT NULL DEFAULT 'plain' COMMENT '消息加解密方式 plain=明文 compatible=兼容 safe=安全' AFTER `aes_key`,
ADD COLUMN `data_format` varchar(16) NOT NULL DEFAULT 'xml' COMMENT '消息数据格式 xml / json' AFTER `encrypt_mode`,
ADD COLUMN `push_reply` varchar(512) NOT NULL DEFAULT '' COMMENT '客服消息自动回复文案(空=不自动回复)' AFTER `data_format`;
-- 结果校验
SELECT id, name, app_id, enabled, token, encrypt_mode, data_format, push_reply
FROM yz_platform_wechat_mini_app
WHERE delete_time IS NULL;
@@ -0,0 +1,45 @@
-- ============================================================
-- 小程序订阅消息:模板配置 + 授权记录
-- yz_platform_wechat_mini_template 订阅消息模板(按小程序 + 场景码)
-- yz_platform_wechat_mini_subscribe 用户授权记录(一次性订阅的剩余发送次数)
--
-- 模板 ID 需要在小程序后台「功能 → 订阅消息 → 公共模板库」选用后填入平台端。
-- fields 为 JSON:微信模板字段键 → 语义变量名,例如
-- {"thing1":"title","phrase2":"content","time3":"time"}
--
-- 幂等脚本:重复执行不会产生重复数据。
-- 执行:mysql -h <host> -P <port> -u <user> -p <db> < create_wechat_mini_template.sql
-- ============================================================
CREATE TABLE IF NOT EXISTS yz_platform_wechat_mini_template (
id bigint unsigned NOT NULL AUTO_INCREMENT,
app_id varchar(64) NOT NULL COMMENT '小程序 AppID',
code varchar(64) NOT NULL COMMENT '场景码 notice/approval/schedule...',
template_id varchar(128) NOT NULL DEFAULT '' COMMENT '微信订阅消息模板 ID',
title varchar(255) NOT NULL DEFAULT '' COMMENT '场景名称',
page varchar(255) NOT NULL DEFAULT '' COMMENT '点击消息跳转的小程序页面',
fields text NULL COMMENT 'JSON:微信字段键 → 语义变量名映射',
enabled tinyint NOT NULL DEFAULT 1 COMMENT '1=启用 0=停用',
remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time datetime NULL COMMENT '更新时间',
delete_time datetime NULL COMMENT '软删时间',
PRIMARY KEY (id),
UNIQUE KEY uk_mini_tpl_app_code (app_id, code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序订阅消息模板';
CREATE TABLE IF NOT EXISTS yz_platform_wechat_mini_subscribe (
id bigint unsigned NOT NULL AUTO_INCREMENT,
identity_id bigint unsigned NOT NULL COMMENT '身份 ID(yz_auth_identity.id)',
app_id varchar(64) NOT NULL COMMENT '小程序 AppID',
template_id varchar(128) NOT NULL COMMENT '微信订阅消息模板 ID',
remain int NOT NULL DEFAULT 0 COMMENT '剩余可发送次数(一次性订阅)',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time datetime NULL COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_mini_sub_identity_tpl (identity_id, app_id, template_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序订阅消息授权记录';
-- 结果校验
SELECT id, app_id, code, template_id, title, page, enabled FROM yz_platform_wechat_mini_template
WHERE delete_time IS NULL;
@@ -0,0 +1,142 @@
# 小程序订阅消息:配置与使用(怎么让用户收到提示)
## 一、先分清两条通道
| | 消息推送(服务器配置) | 订阅消息 |
| --- | --- | --- |
| 方向 | **用户 → 你**(用户发客服消息给你) | **你 → 用户**(你主动推提示给用户) |
| 触发 | 用户在小程序点「客服」发消息 | 业务事件发生(审批待办、日程提醒、站内信…) |
| 前置条件 | 无 | 用户必须在微信弹窗里点「允许」(一次性订阅:授权一次 = 1 次推送机会) |
| 微信后台位置 | 开发管理 → 开发设置 → 消息推送 | 功能 → 订阅消息 |
| 本工程实现 | `services/wechatmini/push.go` + `controllers/api_wechat_mini.go` | `services/wechatmini/subscribe.go` + `controllers/api_mini_subscribe.go` |
**结论**:想让用户「收到提示」,靠的是**订阅消息**,不是消息推送。消息推送只是接收端(顺带做客服自动回复)。
---
## 二、完整流程(4 步)
### 第 1 步:微信后台选一个订阅消息模板
小程序后台 → **功能 → 订阅消息 → 公共模板库 → 选用模板**(例如「审批进度通知」「待办提醒」)。
选完后记下两样东西:
- **模板 ID**(一串字母数字)
- **模板的字段名**(如 `thing1`、`phrase2`、`time3`、`character_string4`)
> 模板选错会导致推送报 40066 / 47003,字段名对不上则报「模板字段不匹配」。
### 第 2 步:平台端登记模板
平台后台 → **系统设置 → 小程序配置 → 编辑小程序 → 订阅消息模板 → 新增模板**
| 字段 | 填什么 |
| --- | --- |
| 场景码 | `notice`(站内通知/通用提醒;同一个小程序下唯一) |
| 场景名称 | 如「站内通知」 |
| 模板 ID | 第 1 步复制的模板 ID |
| 跳转页面 | 用户点击消息进入的小程序页面,如 `pages/index/index` |
| 字段映射 | JSON:`{"thing1":"title","phrase2":"content","time3":"time"}` |
**字段映射怎么读**:左边是微信模板的字段名,右边是平台这边的变量名。平台会提供三个变量:
- `title` —— 标题(如「您有新的审批待办」)
- `content` —— 内容
- `time` —— 推送时间(`2026-10-31 19:00`)
映射里写了哪个字段,就把对应变量填进去;模板有但没映射的字段会自动填 `-`。
### 第 3 步:小程序端让用户授权
已接入的位置:**小程序「我的 → 消息通知」**,点一下会拉起微信的订阅授权弹窗。
```js
import { requestSubscribeNotice } from '@/utils/subscribe.js'
await requestSubscribeNotice()
```
建议在**用户确实可能需要收到提醒**的时机也调用一次(比如进入工作台、提交完审批之后)——因为微信一次性订阅,授权一次只有一次推送机会,用完了要再授权。
小程序端也可以直接调接口:
- `GET /api/mini/subscribe/templates` 取模板列表
- `POST /api/mini/subscribe/authorize` 上报授权结果
- `POST /api/mini/subscribe/test` 给自己发一条测试消息
### 第 4 步:业务侧触发
已接入的触发点(业务照常发,订阅消息自动跟上,**不影响主流程**,失败只记日志):
| 业务 | 代码位置 |
| --- | --- |
| 站内信(平台端手动发通知) | `services/system_sitereminder.go` → `SendSiteReminder` |
| OA 日程提醒(定时任务) | `services/reminder_scheduler.go` → `SiteMsgSender.Send` |
| 审批待办(租户侧审批人生成待办) | `services/approval/engine.go` → `createTasks` |
自己接新场景时,直接调:
```go
wechatmini.PushTenantNotice(租户用户ID, "标题", "内容")
// 或指定场景码:
wechatmini.PushSubscribeToTenantUser(租户用户ID, "notice", "", map[string]string{
"title": "标题", "content": "内容", "time": "2026-10-31 19:00",
})
```
---
## 三、一次性订阅的规则(最容易踩坑)
- 用户点一次「允许」= **1 次**推送机会,服务器记在 `yz_platform_wechat_mini_subscribe.remain`
- 推送成功自动 `remain - 1`;用户拒绝或机会用尽(微信返回 43101)自动清零
- `remain = 0` 时不会调微信接口,直接跳过(不浪费配额)
- 微信一次弹窗最多 3 个模板,前端已自动分批
- 「长期订阅」只有部分类目能用,��务小程序基本用不了,所以要设计好**再次引导授权**的时机
---
## 四、常见错误码
| errcode | 含义 | 处理 |
| --- | --- | --- |
| 43101 | 用户未订阅 / 拒绝订阅 | 引导用户在小程序里重新点「消息通知」授权 |
| 40066 | 模板 ID 无效 | 检查是不是填了「功能 → 订阅消息」里真正的模板 ID |
| 47003 | 模板字段不匹配 | 检查字段映射与微信模板字段是否一致 |
| 40003 | openid 无效 | 该用户不是用这个小程序登录的,或 openid 与 AppID 不匹配 |
| 40001 | access_token 失效 | 工具会自动刷新,稍后重试 |
平台端「测试」按钮会直接返回中文排查建议。
---
## 五、接口清单
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/mini/subscribe/templates` | 小程序端:可订阅模板列表(含剩余次数) |
| POST | `/api/mini/subscribe/authorize` | 小程序端:上报授权结果 |
| POST | `/api/mini/subscribe/test` | 小程序端:给自己发一条测试消息 |
| GET | `/platform/wechatMini/templates` | 平台端:模板列表 |
| POST | `/platform/wechatMini/template/save` | 平台端:保存模板 |
| POST | `/platform/wechatMini/template/delete/:id` | 平台端:删除模板 |
| POST | `/platform/wechatMini/template/test` | 平台端:给指定租户用户发测试消息 |
## 六、相关文件
| 文件 | 作用 |
| --- | --- |
| `go/services/wechatmini/subscribe.go` | 订阅消息 / 客服消息发送、模板 CRUD、授权记录、推送编排 |
| `go/controllers/api_mini_subscribe.go` | 小程序端接口 |
| `go/controllers/platform_wechat_mini.go` | 平台端模板管理接口 |
| `go/models/wechat_mini.go` | `WechatMiniTemplate` / `WechatMiniSubscribe` 模型 |
| `go/docs/sql/create_wechat_mini_template.sql` | 建表脚本 |
| `uniapp/utils/subscribe.js` | 小程序端授权助手 |
| `uniapp/api/subscribe.js` | 小程序端接口封装 |
| `uniapp/pages/profile/profile.vue` | 「我的 → 消息通知」授权入口 |
建表脚本:
```bash
mysql -h <host> -P <port> -u <user> -p <db> < go/docs/sql/create_wechat_mini_template.sql
```
@@ -0,0 +1,135 @@
# 微信小程序「消息推送」配置指引
对应小程序后台:**开发管理 → 开发设置 → 消息推送**(就是你截图里的那个表单)。
---
## 0. 先搞清楚这个配置是干什么的
微信后台的「消息推送」= **你的服务器接收微信主动推送的入口**。
| 场景 | 是否走这个配置 |
| --- | --- |
| 用户给小程序「客服」发消息、进入客服会话 | ✅ 走(微信 POST 到你的 URL) |
| 扫普通链接二维码打开小程序等事件 | ✅ 走 |
| 订阅消息(`wx.requestSubscribeMessage`) | ❌ 不走,用服务端 API 主动发 |
| 模板消息 | ❌ 已下线,改用订阅消息 |
---
## 1. 前置条件(不满足会一直「配置失败」)
1. **域名已 ICP 备案**(微信后台会校验)。
2. **只用 80 / 443 端口**:`https://xxx.com`(443)或 `http://xxx.com`(80),其它端口微信不接受。
3. **微信服务器能直连**:公网可达,不能是 `127.0.0.1`、内网 IP 或本机端口。
4. **HTTPS 证书链完整**:只部署 `.crt` 不带中间证书,微信侧会握手失败(自签证书不行)。
5. **5 秒内返回**:接口要在 5 秒内响应,否则微信断开并重试。
> 本项目的回调地址在 `go/conf/app.conf` 里配置:
> `wechat_mini_callback_base = https://api.yunzer.cn`(留空则退化为相对路径 `/api/wechat/mini/callback`,需由网关补域名)。
---
## 2. 第一步:在本平台生成参数
平台端:**系统设置 → 小程序配置 → 编辑某个小程序 → 消息推送**
| 字段 | 操作 | 说明 |
| --- | --- | --- |
| 回调地址 | 自动生成,点「复制」 | 即 `https://你的域名/api/wechat/mini/callback` |
| Token | 点「随机生成」 | 3-32 位字母数字 |
| EncodingAESKey | 点「随机生成」 | 43 位字母数字,后台也有「随机生成」按钮,两边值必须一致 |
| 加解密方式 | 明文 / 兼容 / 安全 | 与微信后台选择保持一致 |
| 数据格式 | XML / JSON | 与微信后台选择保持一致 |
| 自动回复文案 | 选填 | 留空 = 只接收不回复 |
填完点 **「保存消息推送配置」**(注意:这一步只存平台端,不会自动同步到微信后台)。
---
## 3. 第二步:在微信后台把值抄过去
小程序后台 → 开发管理 → 开发设置 → 消息推送,逐项对应:
| 后台字段 | 填什么 |
| --- | --- |
| URL(服务器地址) | 平台端「回调地址」整条复制,如 `https://api.yunzer.cn/api/wechat/mini/callback` |
| Token(令牌) | 平台端「Token」 |
| EncodingAESKey(消息加密密钥) | 平台端「EncodingAESKey」(43 位) |
| 消息加密方式 | 与平台端一致(明文 / 兼容 / 安全) |
| 数据格式 | 与平台端一致(XML / JSON) |
填完点「提交」,微信会立刻发一个 **GET** 请求到你的 URL 做校验,通过就提示配置成功。
⚠️ 配置**提交后立即生效**,切换加密方式或数据格式前,务必先把平台端改成同样的值,否则消息会被验签/解密失败直接丢弃。
---
## 4. 提交时微信做了什么(校验逻辑)
微信 GET 你的 URL,参数:`signature` `timestamp` `nonce` `echostr`。
- **明文 / 兼容模式**:服务端算 `sha1(sort(token, timestamp, nonce) 拼接)` 与 `signature` 比对,通过后**原样返回 `echostr`**。
- **安全模式**:`echostr` 是密文,额外带 `msg_signature = sha1(sort(token, timestamp, nonce, echostr))`;服务端验签后解密,再返回解密结果。
对应实现:
- `go/services/wechatmini/push.go` —— 签名校验、AES 加解密、XML/JSON 解析、被动回复构造
- `go/controllers/api_wechat_mini.go` —— `GET /api/wechat/mini/callback` 校验 + `POST` 收消息
**多小程序**:每个小程序在自己的后台单独配置,本平台按 AppID 分别维护 Token / AESKey;
回调接口不携带 appid,服务端会逐个应用匹配签名,命中哪个就按哪个的密钥解密。
---
## 5. 收到消息后的行为
- 微信 **5 秒**内没收到响应会断开并重试,**总共重试 3 次**(间隔 15s / 5s / 5s)。
- 返回**空串**或 `success` = 已处理,微信不再重试(我们的实现返回空串)。
- 平台端配了「自动回复文案」时,5 秒内以被动回复形式把文案回给用户。
- 复杂业务(例如按关键字回复、转发人工)可接管钩子:
`services/wechatmini/push.go` 中的 `PushHandler`。
---
## 6. 常见报错排查
| 现象 | 常见原因 | 处理 |
| --- | --- | --- |
| 后台一直「配置失败」 | 域名未备案 / 端口不是 80、443 / 证书链不完整 / 返回内容不是 echostr | 逐条对照第 1 节 |
| 后台提示超时 | 接口处理超过 5 秒 | 先只回 echostr,业务逻辑异步做 |
| 后台提示 Token 校验失败 | 平台端 Token 与后台填的不一致 | 两边都复制一遍核对(区分大小写) |
| 配置成功但收不到消息 | 加解密方式或数据格式两边不一致 | 平台端与后台改成一致 |
| 小程序会话提示「客服暂时无法提供服务」 | 5 秒内没回复 | 配「自动回复文案」,或让接口先返回空串 |
| 明文模式正常,切安全模式收不到 | EncodingAESKey 两边不一致 | 重新复制 43 位密钥 |
### 自测命令(明文模式)
```bash
# signature = sha1(sort(token, timestamp, nonce) 拼接),用与平台端相同的 token
curl "https://api.yunzer.cn/api/wechat/mini/callback?signature=<签名>&timestamp=1700000000&nonce=abc123&echostr=hello"
# 期望输出:hello(等于传入的 echostr)
```
服务端日志会打印 `[wechat-mini] 收到推送: ...`,看不到这行说明微信请求没到服务端(先查域名 / 端口 / 证书)。
---
## 7. 相关接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET/POST | `/api/wechat/mini/callback` | 微信回调(无需鉴权,内部验签) |
| GET | `/platform/wechatMini/app/push?app_id=xxx` | 读取推送配置(含回调地址) |
| POST | `/platform/wechatMini/app/push/save` | 保存推送配置 |
| POST | `/platform/wechatMini/app/push/generate` | 生成随机 Token / EncodingAESKey |
## 8. 数据库
新增字段(`token` / `aes_key` / `encrypt_mode` / `data_format` / `push_reply`):
```bash
mysql -h <host> -P <port> -u <user> -p <db> < go/docs/sql/alter_wechat_mini_app_push.sql
```
`aes_key` 以 AES-256-GCM 加密入库(复用公众号的 `wechat_mp_secret_key`),接口只回显掩码。
+94
View File
@@ -0,0 +1,94 @@
# 服务器增量部署说明(update-server.bat)
本地 `go/` 目录改动后,只把**改动过的文件**推到服务器,不用再一个个找文件替换。
## 一、用法
在 `go/` 目录下双击 `update-server.bat`,或命令行执行:
```bat
update-server.bat :: 比对 -> 确认 -> 上传 -> 询问是否重启服务
update-server.bat dry :: 只比对不上传(先看清单用这个)
update-server.bat restart :: 只重启服务器服务
```
bat 会在 `go/` 目录下自动编译部署工具 `bin/deploy.exe`,然后:
1. SSH 连上服务器,对 `go/` 目录和服务器目录**各算一遍文件 MD5**
2. 比对出「新增」和「内容变了」的文件并打印清单
3. 确认后把这些文件打包成 tar 通过 SSH 管道传到服务器解包(只传差异文件)
4. 回读服务器指纹复核,确认每个文件都传对了
5. 询问是否 `systemctl restart go-api.service` 让改动生效
## 二、配置
bat 顶部「CONFIG」区:
| 变量 | 说明 |
| --- | --- |
| `DEPLOY_HOST` | 服务器地址 |
| `DEPLOY_USER` | SSH 用户名 |
| `DEPLOY_DIR` | 服务器目录 |
| `DEPLOY_KEY` | 私钥路径(默认 `%~dp010.31.100.2_id_ed25519`) |
| `DEPLOY_PASS` | 密码(备用,服务器开了密码认证时才用) |
| `RESTART_CMD` | 重启命令,默认 `systemctl restart go-api.service` |
> **注意**:这台服务器 sshd 关闭了密码认证(只接受 publickey / gssapi),所以默认走**密钥登录**。
> 如果以后在服务器上开了密码认证,把 `DEPLOY_KEY` 置空、填上 `DEPLOY_PASS` 即可,工具会自动切换。
### 关于私钥
- 私钥放在 `go/10.31.100.2_id_ed25519`,**已被同步工具自动排除**,不会传到服务器。
- Windows 上私钥权限过宽会让 OpenSSH 客户端拒绝加载,已用下面的命令收紧过(只需做一次):
```bat
icacls "10.31.100.2_id_ed25519" /inheritance:r /grant:r "%USERNAME%:(F)"
```
- `go/.deploy_known_hosts` 记录服务器主机密钥指纹,换服务器时会提示指纹变化(同样被排除,不会上传)。
## 三、不会同步的内容
工具默认跳过:`.git` / `node_modules` / `bin` / `dist` / `tmp`、`*.exe` / `*.log` / `*.bak`、
`nohup.out`、`output.log`、`go.log`、`test-output-*.txt`、`migrate_*.csv`、
无扩展名的 `server` 二进制、`*_id_ed25519` 私钥、`.deploy_known_hosts`。
服务器上存在、本地没有的文件**默认保留不删**(服务器上还有 `.git`、历史二进制等)。
确实要清理就在 bat 里加 `-delete`,但请先想清楚。
## 四、原理
```
本地 go/ ──MD5──┐
├─ 比对 ──> 只传 新增/变更 的文件 ──> 服务器 /www/wwwroot/api.yunzer.cn
服务器 ──MD5──┘ └── 传完回读远端指纹复核
```
- 服务器是 systemd 的 `go-api.service`,`ExecStart=/usr/local/btgo/bin/go run main.go`,
**直接用目录里的源码编译运行**,所以传 `.go` 源码即可,重启服务就会重新编译生效。
- 传到服务器的文件属主会被改成目标目录的属主(`www:www`),权限规范成 0644(脚本 0755)。
## 五、常见问题
| 现象 | 处理 |
| --- | --- |
| `Permission denied (publickey,gssapi-...)` | 私钥没配好:检查 `DEPLOY_KEY` 路径,或用 icacls 收紧权限 |
| `本地目录不存在` | 别从别处调用 bat,bat 里用 `%~dp0` 定位自身目录 |
| 上传后服务没生效 | 需要重启:`update-server.bat restart`(或看 go.log 是否有编译错误) |
| 清单里出现奇怪的文件 | 用配置区的 `EXTRA_EXCLUDE` 排除,逗号分隔、支持 `*` |
| 远端提示 md5sum 不存在 | 极老系统没 md5sum,用 `cksum` 也能工作,可改工具里的命令 |
## 六、单独使用部署工具
bat 之外的进阶用法(在 `go/` 目录下):
```bat
bin\deploy.exe -host 10.31.100.2 -user root -key 10.31.100.2_id_ed25519 ^
-dir /www/wwwroot/api.yunzer.cn -local . -dry-run :: 只看差异
bin\deploy.exe -host 10.31.100.2 -user root -key 10.31.100.2_id_ed25519 ^
-exec "systemctl status go-api.service" :: 执行远程命令
```
常用参数:`-dry-run` 只比对、`-yes` 跳过确认、`-delete` 同步删除远端多余文件、
`-restart "命令"` 上传后自动重启、`-exclude "a,b"` 额外排除。