318 lines
8.2 KiB
Go
318 lines
8.2 KiB
Go
// 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 // 明文
|
||
QrcodeURL 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),
|
||
QrcodeURL: strings.TrimSpace(row.QrcodeURL),
|
||
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
|
||
}
|
||
|
||
// PublicApp 对外公布的小程序信息(供官网 / H5 等其他端展示二维码)
|
||
type PublicApp struct {
|
||
ID uint64 `json:"id"`
|
||
Name string `json:"name"`
|
||
AppID string `json:"app_id"`
|
||
QrcodeURL string `json:"qrcode_url"`
|
||
}
|
||
|
||
// ListEnabledApps 已启用的小程序(含二维码地址),供其他端公开读取
|
||
func ListEnabledApps() ([]PublicApp, error) {
|
||
var rows []models.WechatMiniApp
|
||
_, err := models.Orm.QueryTable(new(models.WechatMiniApp)).
|
||
Filter("delete_time__isnull", true).
|
||
Filter("enabled", 1).
|
||
OrderBy("id").
|
||
All(&rows)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
list := make([]PublicApp, 0, len(rows))
|
||
for _, r := range rows {
|
||
list = append(list, PublicApp{
|
||
ID: r.ID,
|
||
Name: strings.TrimSpace(r.Name),
|
||
AppID: strings.TrimSpace(r.AppID),
|
||
QrcodeURL: strings.TrimSpace(r.QrcodeURL),
|
||
})
|
||
}
|
||
return list, nil
|
||
}
|
||
|
||
// SaveAppInput 保存应用入参(AppSecret 支持掩码表示不修改)
|
||
type SaveAppInput struct {
|
||
ID uint64
|
||
Name string
|
||
AppID string
|
||
AppSecret string // 掩码 / 空串 = 保持不变
|
||
QrcodeURL 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.QrcodeURL = strings.TrimSpace(in.QrcodeURL)
|
||
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,
|
||
QrcodeURL: row.QrcodeURL, // 旧端点不改动二维码
|
||
Enabled: in.Enabled,
|
||
AutoBind: in.AutoBind,
|
||
Remark: in.Remark,
|
||
})
|
||
return serr
|
||
}
|