Files
2026-10-21 17:45:50 +08:00

318 lines
8.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}