// Package wechatmini 微信小程序服务端能力:配置、code 换 openid、手机号快速验证。 // // 与 services/wechatmp(公众号)分开:小程序走 sns/jscode2session 与 // wxa/business/getuserphonenumber,两者的 AppID / AppSecret 互不相通。 // // 配置存储:多应用表 yz_platform_wechat_mini_app(docs/sql/create_wechat_mini_app.sql), // 旧单行表 yz_platform_wechat_mini_config 仅作为未迁移部署的回退数据源。 package wechatmini import ( "errors" "fmt" "strings" "time" "github.com/beego/beego/v2/client/orm" "server/models" "server/services/wechatmp" ) // Config 小程序运行配置(敏感字段已解密) type Config struct { ID uint64 Name string AppID string AppSecret string // 明文 Enabled bool AutoBind bool Remark string } // ErrNotConfigured 未配置或未启用 var ErrNotConfigured = errors.New("微信小程序未配置或未启用") // appFromRow 解密行数据为运行配置 func appFromRow(row models.WechatMiniApp) (*Config, error) { cfg := &Config{ ID: row.ID, Name: row.Name, AppID: strings.TrimSpace(row.AppID), Enabled: row.Enabled == 1, AutoBind: row.AutoBind != 0, Remark: row.Remark, } if row.AppSecret != "" { plain, derr := wechatmp.DecryptSecret(row.AppSecret) if derr != nil { return nil, derr } cfg.AppSecret = plain } return cfg, nil } // LoadConfig 读取小程序配置(多应用表第一条,敏感字段解密)。 // 新表无数据时回退旧单行表(未执行迁移 SQL 的存量部署);均未配置返回 ErrNotConfigured func LoadConfig() (*Config, error) { var rows []models.WechatMiniApp _, err := models.Orm.QueryTable(new(models.WechatMiniApp)). Filter("delete_time__isnull", true). OrderBy("id"). All(&rows) if err == nil && len(rows) > 0 { return appFromRow(rows[0]) } // 旧单行表回退 var legacy models.WechatMiniConfig err = models.Orm.QueryTable(new(models.WechatMiniConfig)).OrderBy("id").One(&legacy) if err != nil { if err == orm.ErrNoRows { return nil, ErrNotConfigured } return nil, err } cfg := &Config{ ID: legacy.ID, AppID: strings.TrimSpace(legacy.AppID), Enabled: legacy.Enabled == 1, AutoBind: legacy.AutoBind != 0, Remark: legacy.Remark, } if legacy.AppSecret != "" { plain, derr := wechatmp.DecryptSecret(legacy.AppSecret) if derr != nil { return nil, derr } cfg.AppSecret = plain } return cfg, nil } // LoadEnabledConfig 读取并校验启用状态 func LoadEnabledConfig() (*Config, error) { cfg, err := LoadConfig() if err != nil { return nil, err } if !cfg.Enabled || cfg.AppID == "" || cfg.AppSecret == "" { return nil, ErrNotConfigured } return cfg, nil } // LoadEnabledAppByAppID 按小程序 AppID 精确读取启用配置(多小程序场景, // 认证端按客户端传入的 appid 选择对应应用) func LoadEnabledAppByAppID(appID string) (*Config, error) { appID = strings.TrimSpace(appID) if appID == "" { return nil, ErrNotConfigured } var row models.WechatMiniApp err := models.Orm.QueryTable(new(models.WechatMiniApp)). Filter("app_id", appID). Filter("delete_time__isnull", true). One(&row) if err != nil { if err == orm.ErrNoRows { return nil, ErrNotConfigured } return nil, err } cfg, err := appFromRow(row) if err != nil { return nil, err } if !cfg.Enabled || cfg.AppSecret == "" { return nil, ErrNotConfigured } return cfg, nil } // ListApps 应用列表(软删除外,AppSecret 不回显) func ListApps() ([]models.WechatMiniApp, error) { var rows []models.WechatMiniApp _, err := models.Orm.QueryTable(new(models.WechatMiniApp)). Filter("delete_time__isnull", true). OrderBy("id"). All(&rows) return rows, err } // SaveAppInput 保存应用入参(AppSecret 支持掩码表示不修改) type SaveAppInput struct { ID uint64 Name string AppID string AppSecret string // 掩码 / 空串 = 保持不变 Enabled bool AutoBind bool Remark string } // SaveApp 新增/更新小程序应用(AppID 全局唯一,排除自身) func SaveApp(in SaveAppInput) (uint64, error) { appID := strings.TrimSpace(in.AppID) if appID == "" { return 0, errors.New("AppID 不能为空") } now := time.Now() var row models.WechatMiniApp if in.ID > 0 { err := models.Orm.QueryTable(new(models.WechatMiniApp)). Filter("id", in.ID). Filter("delete_time__isnull", true). One(&row) if err != nil { return 0, errors.New("应用不存在或已删除") } } else { row = models.WechatMiniApp{CreateTime: now} } // AppID 唯一性校验(排除自身) dupQ := models.Orm.QueryTable(new(models.WechatMiniApp)). Filter("app_id", appID). Filter("delete_time__isnull", true) if in.ID > 0 { dupQ = dupQ.Filter("id__ne", in.ID) } var dup models.WechatMiniApp if err := dupQ.One(&dup); err == nil { name := strings.TrimSpace(dup.Name) if name == "" { name = dup.AppID } return 0, fmt.Errorf("AppID 已被应用「%s」占用", name) } row.Name = strings.TrimSpace(in.Name) row.AppID = appID secret := strings.TrimSpace(in.AppSecret) if secret != "" && !wechatmp.IsMasked(secret) { enc, eerr := wechatmp.EncryptSecret(secret) if eerr != nil { return 0, eerr } row.AppSecret = enc } row.Enabled = 0 if in.Enabled { row.Enabled = 1 } row.AutoBind = 0 if in.AutoBind { row.AutoBind = 1 } row.Remark = strings.TrimSpace(in.Remark) row.UpdateTime = &now if row.ID == 0 { id, err := models.Orm.Insert(&row) return uint64(id), err } if _, err := models.Orm.Update(&row); err != nil { return 0, err } return row.ID, nil } // DeleteApp 删除应用(软删并停用) func DeleteApp(id uint64) error { if id == 0 { return errors.New("无效ID") } now := time.Now() _, err := models.Orm.QueryTable(new(models.WechatMiniApp)). Filter("id", id). Filter("delete_time__isnull", true). Update(map[string]interface{}{ "delete_time": now, "update_time": now, "enabled": 0, }) return err } // SaveInput 旧单行配置保存入参(AppSecret 支持掩码表示不修改)——兼容旧端点, // 现写入新多应用表的第一行(无行时插入) type SaveInput struct { AppID string AppSecret string // 掩码 / 空串 = 保持不变 Enabled bool AutoBind bool Remark string } // SaveConfig 保存配置(兼容旧单行端点:写入新表第一行,不存在则创建) func SaveConfig(in SaveInput) error { var row models.WechatMiniApp err := models.Orm.QueryTable(new(models.WechatMiniApp)). Filter("delete_time__isnull", true). OrderBy("id"). One(&row) if err == orm.ErrNoRows { _, serr := SaveApp(SaveAppInput{ Name: "默认小程序", AppID: in.AppID, AppSecret: in.AppSecret, Enabled: in.Enabled, AutoBind: in.AutoBind, Remark: in.Remark, }) return serr } if err != nil { return err } _, serr := SaveApp(SaveAppInput{ ID: row.ID, Name: row.Name, AppID: in.AppID, AppSecret: in.AppSecret, Enabled: in.Enabled, AutoBind: in.AutoBind, Remark: in.Remark, }) return serr }