优化小程序部分

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

+5
View File
@@ -41,6 +41,11 @@ func Register() {
// 日程提醒确认接口
beego.Router("/api/schedule/reminder/ack", &controllers.ApiReminderController{}, "get:AckReminder")
// 小程序订阅消息:取模板 / 上报授权 / 测试发送
beego.Router("/api/mini/subscribe/templates", &controllers.AppMiniSubscribeController{}, "get:Templates")
beego.Router("/api/mini/subscribe/authorize", &controllers.AppMiniSubscribeController{}, "post:Authorize")
beego.Router("/api/mini/subscribe/test", &controllers.AppMiniSubscribeController{}, "post:TestNotice")
// ===== 移动端(uniapp)登录相关(AppAuthController)=====
beego.Router("/api/login", &controllers.AppAuthController{}, "post:LoginBackend")
beego.Router("/api/sendLoginCode", &controllers.AppAuthController{}, "post:SendLoginCode")
+3
View File
@@ -14,6 +14,9 @@ func Register() {
// 微信公众号(服务号):配置 / 绑定 / 粉丝 / 推送 / 微信服务器回调
RegisterWechatMpRoutes()
// 微信小程序:应用配置 / 消息推送回调 / 消息推送参数
RegisterWechatMiniRoutes()
// 数据字典(字典列表 / 字典项)
RegisterDictRoutes()
+32
View File
@@ -0,0 +1,32 @@
package platform
import (
"server/controllers"
beego "github.com/beego/beego/v2/server/web"
)
// RegisterWechatMiniRoutes 注册微信小程序相关路由。
// 注意:/api/wechat/mini/callback 供微信服务器回调,无平台鉴权(控制器内完成签名校验)。
func RegisterWechatMiniRoutes() {
// 微信消息推送回调(URL 填入小程序后台「开发管理 → 开发设置 → 消息推送」)
beego.Router("/api/wechat/mini/callback", &controllers.WechatMiniCallbackController{}, "get:Callback;post:Callback")
// 小程序配置(单行兼容端点;多应用管理见下)
beego.Router("/platform/wechatMini/config", &controllers.PlatformWechatMiniController{}, "get:GetConfig;post:SaveConfig")
// 小程序多应用管理
beego.Router("/platform/wechatMini/apps", &controllers.PlatformWechatMiniController{}, "get:ListApps")
beego.Router("/platform/wechatMini/app/save", &controllers.PlatformWechatMiniController{}, "post:SaveApp")
beego.Router("/platform/wechatMini/app/delete/:id", &controllers.PlatformWechatMiniController{}, "post:DeleteApp")
// 消息推送配置(Token / EncodingAESKey / 加解密方式 / 数据格式 / 自动回复文案)
beego.Router("/platform/wechatMini/app/push", &controllers.PlatformWechatMiniController{}, "get:GetPushConfig")
beego.Router("/platform/wechatMini/app/push/save", &controllers.PlatformWechatMiniController{}, "post:SavePushConfig")
beego.Router("/platform/wechatMini/app/push/generate", &controllers.PlatformWechatMiniController{}, "post:GeneratePushSecret")
// 订阅消息模板(小程序后台「功能 → 订阅消息」选用模板后在此登记)
beego.Router("/platform/wechatMini/templates", &controllers.PlatformWechatMiniController{}, "get:ListTemplates")
beego.Router("/platform/wechatMini/template/save", &controllers.PlatformWechatMiniController{}, "post:SaveTemplate")
beego.Router("/platform/wechatMini/template/delete/:id", &controllers.PlatformWechatMiniController{}, "post:DeleteTemplate")
beego.Router("/platform/wechatMini/template/test", &controllers.PlatformWechatMiniController{}, "post:TestTemplate")
}
+15
View File
@@ -0,0 +1,15 @@
package platform
import "testing"
// TestRegisterWechatMiniRoutes 确认小程序路由能完成注册且不与既有路由冲突。
// beego 在注册重复/歧义路由或非法方法名时会 panic,这里通过实际注册来兜住这类错误。
func TestRegisterWechatMiniRoutes(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("注册平台端小程序路由失败: %v", r)
}
}()
RegisterWechatMiniRoutes()
}
-7
View File
@@ -38,11 +38,4 @@ func RegisterWechatMpRoutes() {
// 消息推送(认证后可用:提醒 / 公告)
beego.Router("/platform/wechatMp/push", &controllers.PlatformWechatMpController{}, "post:Push")
beego.Router("/platform/wechatMp/push/test", &controllers.PlatformWechatMpController{}, "post:PushTest")
// 小程序配置(一键登录开关;登录端点在认证中心 /auth/wechat/miniapp/*)
beego.Router("/platform/wechatMini/config", &controllers.PlatformWechatMiniController{}, "get:GetConfig;post:SaveConfig")
// 小程序多应用管理(支持配置多个小程序)
beego.Router("/platform/wechatMini/apps", &controllers.PlatformWechatMiniController{}, "get:ListApps")
beego.Router("/platform/wechatMini/app/save", &controllers.PlatformWechatMiniController{}, "post:SaveApp")
beego.Router("/platform/wechatMini/app/delete/:id", &controllers.PlatformWechatMiniController{}, "post:DeleteApp")
}
+21
View File
@@ -95,6 +95,27 @@ func init() {
api.Register()
}
// 根路径站点验证文件(备案 / 搜索引擎 / 第三方平台的域名所有权验证)。
// 详见 routers/verifyfiles.go 注释。
// 由 app.conf 的 verify_dir / verify_exts / verify_hosts 控制:
// verify_dir 验证文件目录,留空 = 默认 verify(相对服务目录);填 - = 关闭
// verify_exts 允许的扩展名,留空 = 默认 .txt,.html,.htm
// verify_hosts 限定生效域名,留空 = 所有走本服务的域名都生效
// 必须注册在 RegisterPlatformSPA 之前:两者都是 BeforeStatic 过滤器,
// 先注册者先执行,避免验证文件被 SPA 的 index.html 兜底吃掉。
if verifyDir, _ := beego.AppConfig.String("verify_dir"); strings.TrimSpace(verifyDir) != "-" {
verifyExts, _ := beego.AppConfig.String("verify_exts")
var verifyHosts []string
if raw, _ := beego.AppConfig.String("verify_hosts"); raw != "" {
for _, h := range strings.Split(raw, ",") {
if t := strings.TrimSpace(h); t != "" {
verifyHosts = append(verifyHosts, t)
}
}
}
RegisterVerifyFiles(verifyDir, verifyExts, verifyHosts...)
}
// 平台后台 SPA 静态托管(详见 routers/spa.go 注释)。
// 由 app.conf 的 platform_spa_dir / platform_spa_hosts 控制;任一为空则不启用。
if spaDir, _ := beego.AppConfig.String("platform_spa_dir"); strings.TrimSpace(spaDir) != "" {
+117
View File
@@ -0,0 +1,117 @@
package routers
import (
"log"
"net/http"
"os"
"path/filepath"
"strings"
"server/pkg/verifyfiles"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/context"
)
// appRoot 定位服务根目录(conf/app.conf 所在目录的上一级)。
//
// beego 找配置的顺序是:当前工作目录 → 可执行文件所在目录。
// 这里沿用同样的顺序,保证不管是用 `go run main.go`(systemd 的 WorkingDirectory)
// 还是编译后的二进制、不管从哪个目录启动,验证文件目录都稳定落在服务根目录下,
// 不会出现「换个目录启动就找不到验证文件」的情况。
//
// 线上即 /www/wwwroot/api.yunzer.cn(conf/app.conf 在 /www/wwwroot/api.yunzer.cn/conf/)。
func appRoot() string {
candidates := make([]string, 0, 2)
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates, wd)
}
if beego.AppPath != "" {
candidates = append(candidates, beego.AppPath)
}
return appRootFrom(candidates)
}
// appRootFrom 在候选目录里找第一个含 conf/app.conf 的目录(即服务根目录)。
func appRootFrom(candidates []string) string {
for _, base := range candidates {
if base == "" {
continue
}
if _, err := os.Stat(filepath.Join(base, "conf", "app.conf")); err == nil {
return base
}
}
return ""
}
// RegisterVerifyFiles 让 Go 直接吐「根路径站点验证文件」。
//
// 背景:nginx 对所有域名都是全量反代给 Go(location / → proxy_pass),
// Go 不认识的路径一律回 beego 404 页,于是这些文件根本访问不到:
// - 备案 / 域名所有权验证:1XXl8fPHr9.txt(阿里云、腾讯云)
// - 搜索引擎站点验证:googleXXXX.html、msXXXXXXXX.txt、Bing 的 .xml
// - 第三方平台域名校验:各平台给的 .txt / .html
//
// 这些文件名每次都不同(随机串),逐个注册路由不现实;改 nginx 又会因为
// 宝塔重生成 vhost 被冲掉(同 routers/spa.go 里遇到的问题)。
// 所以由 Go 统一托管:只要把验证文件丢进 verify 目录,
// https://api.yunzer.cn/1XXl8fPHr9.txt 就能直接取到。
//
// 规则:
// - 只处理 GET / HEAD;
// - 只认「根路径下的单段文件名」,如 /1XXl8fPHr9.txt;
// - 文件名过白名单正则,扩展名在允许列表内(默认 .txt/.html/.htm);
// - 文件必须真实存在于验证目录,否则放行给正常路由(最终仍是 beego 404)。
//
// 验证文件目录由 app.conf 的 verify_dir 指定(默认相对进程工作目录的 verify,
// 即线上服务目录 /www/wwwroot/api.yunzer.cn/verify);verify_hosts 可限定生效域名,
// 留空表示所有走本服务的域名都生效(备案常常同时要过 api.yunzer.cn 与官网主域名)。
func RegisterVerifyFiles(dir string, exts string, hosts ...string) {
dir = strings.TrimSpace(dir)
if dir == "-" {
// 显式关闭
return
}
if dir == "" {
// 默认:服务根目录下的 verify/(app.conf 所在目录的上一级)
dir = filepath.Join(appRoot(), verifyfiles.DefaultDir)
}
resolver, err := verifyfiles.New(verifyfiles.Config{
Dir: dir,
Exts: exts,
Hosts: strings.Join(hosts, ","),
})
if err != nil {
log.Printf("[verify] 根路径验证文件功能未启用: %v", err)
return
}
if resolver == nil {
// dir = "-":显式关闭
return
}
// 目录不存在就建一个,方便直接往里丢验证文件
if err := os.MkdirAll(resolver.Dir(), 0o755); err != nil {
log.Printf("[verify] 创建验证文件目录失败: %v", err)
}
beego.InsertFilter("*", beego.BeforeStatic, func(ctx *context.Context) {
if ctx.Request.Method != http.MethodGet && ctx.Request.Method != http.MethodHead {
return
}
if !resolver.AllowHost(ctx.Request.Host) {
return
}
full, ok := resolver.Resolve(ctx.Request.URL.Path)
if !ok {
return
}
// 文件内容不变,但可能被替换(换 token、换文件名),短缓存即可
ctx.Output.Header("Cache-Control", "public, max-age=300")
http.ServeFile(ctx.ResponseWriter, ctx.Request, full)
})
log.Printf("[verify] 根路径验证文件目录: %s,允许扩展名: %v", resolver.Dir(), resolver.Exts())
}
+115
View File
@@ -0,0 +1,115 @@
package routers
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"server/pkg/verifyfiles"
beego "github.com/beego/beego/v2/server/web"
)
// TestVerifyFilesEndToEnd 走真实 HTTP 链路验证:
// 根路径下的验证文件能被吐出来,且不带白名单的路径仍然 404。
func TestVerifyFilesEndToEnd(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "1XXl8fPHr9.txt"), []byte("1XXl8fPHr9"), 0o644); err != nil {
t.Fatalf("准备测试文件失败: %v", err)
}
// 注册到全局 beego(与线上 router.go 的注册方式一致)
RegisterVerifyFiles(dir, "")
beego.BeeApp.Handlers.Init()
// 1) 验证文件正常返回
req := httptest.NewRequest(http.MethodGet, "/1XXl8fPHr9.txt", nil)
req.Host = "api.yunzer.cn"
rec := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("验证文件应返回 200,实际 %d", rec.Code)
}
if got := rec.Body.String(); got != "1XXl8fPHr9" {
t.Fatalf("验证文件内容不对,实际 %q", got)
}
if ct := rec.Header().Get("Content-Type"); ct == "" || ct[:len("text/plain")] != "text/plain" {
t.Logf("Content-Type: %s", ct)
}
// 2) 不存在的验证文件仍走正常路由(404),行为不变
req2 := httptest.NewRequest(http.MethodGet, "/not-exist-verify.txt", nil)
req2.Host = "api.yunzer.cn"
rec2 := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(rec2, req2)
if rec2.Code != http.StatusNotFound {
t.Fatalf("不存在的验证文件应返回 404,实际 %d", rec2.Code)
}
}
// TestAppRootFrom 确认服务根目录按「含 conf/app.conf 的目录」定位。
func TestAppRootFrom(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "conf"), 0o755); err != nil {
t.Fatalf("准备测试目录失败: %v", err)
}
if err := os.WriteFile(filepath.Join(root, "conf", "app.conf"), []byte("appname = server"), 0o644); err != nil {
t.Fatalf("准备测试文件失败: %v", err)
}
other := t.TempDir()
if got := appRootFrom([]string{other, root}); got != root {
t.Fatalf("应定位到 %s,实际 %s", root, got)
}
if got := appRootFrom([]string{other, ""}); got != "" {
t.Fatalf("没有 conf/app.conf 时应返回空串,实际 %s", got)
}
if got := appRootFrom([]string{""}); got != "" {
t.Fatalf("空候选应返回空串,实际 %s", got)
}
}
// TestDefaultDirResolvesToRepoVerifyDir 确认默认目录落在「服务根目录/verify」下,
// 且仓库里的验证文件能被解析出来、内容正确。
// 线上等价:服务根目录 /www/wwwroot/api.yunzer.cn → verify/1XXl8fPHr9.txt。
func TestDefaultDirResolvesToRepoVerifyDir(t *testing.T) {
orig, err := os.Getwd()
if err != nil {
t.Fatalf("取当前工作目录失败: %v", err)
}
defer os.Chdir(orig)
// 切到 go/ 目录:这里有 conf/app.conf,等价于线上服务根目录
goRoot := filepath.Dir(orig)
if err := os.Chdir(goRoot); err != nil {
t.Fatalf("切换工作目录失败: %v", err)
}
root := appRoot()
if root == "" {
t.Fatalf("应能从 conf/app.conf 定位服务根目录(工作目录 %s)", goRoot)
}
dir := filepath.Join(root, verifyfiles.DefaultDir)
resolver, err := verifyfiles.New(verifyfiles.Config{Dir: dir})
if err != nil || resolver == nil {
t.Fatalf("创建 Resolver 失败: %v", err)
}
full, ok := resolver.Resolve("/1XXl8fPHr9.txt")
if !ok {
t.Fatalf("默认目录 %s 下应能解析出验证文件", dir)
}
b, err := os.ReadFile(full)
if err != nil {
t.Fatalf("读取验证文件失败: %v", err)
}
if string(b) != "1XXl8fPHr9" {
t.Fatalf("验证文件内容不对,实际 %q", string(b))
}
}