2026-09-29 10:57:01 +08:00
|
|
|
|
package logic
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"strconv"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
|
|
|
|
"github.com/gogf/gf/v2/os/gfile"
|
|
|
|
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
|
|
|
|
|
|
|
|
|
|
v1 "tool-api/api/admin/v1"
|
|
|
|
|
|
"tool-api/internal/consts"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 年会域:活动小程序码(wxacode.getUnlimited)
|
|
|
|
|
|
// 依据:PRD-02 R7-11/R7-24;架构 §5.2 A18、§8.3。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 约定:
|
|
|
|
|
|
// - scene **只承载 event_id**(不含任何个人信息);
|
|
|
|
|
|
// - access_token 复用 xpay.go 的 wxAccessToken(),不重造;
|
|
|
|
|
|
// - 未配置 wx.appId / wx.appSecret → 返回明确中文提示,不报错不 panic。
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// qrCodePage 小程序码落地页(扫码后进入的查座页分包页面)。
|
|
|
|
|
|
// 属需环境化的集成参数:上线前需确认该页面已在现网发布。
|
|
|
|
|
|
const qrCodePage = "pkg-event/query"
|
|
|
|
|
|
|
|
|
|
|
|
// QrCodeDir 码图落盘目录(相对服务运行目录)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ 导出给 cmd 包使用,不要改回私有常量:该目录此前只在「生成码图」时惰性创建
|
|
|
|
|
|
// (见 AdminEventQrcode 里的 gfile.Mkdir),而 cmd.go 的静态路由 AddStaticPath
|
|
|
|
|
|
// 要求目录**在注册时必须已存在**,否则直接 FATAL 退出且不降级 ——
|
|
|
|
|
|
// 于是出现「服务起不来 → 生不了码图 → 目录建不出来」的先有鸡先有蛋死锁。
|
|
|
|
|
|
// 现由 cmd.go 启动时预创建(gfile.Mkdir = MkdirAll,已存在则返回 nil)。
|
|
|
|
|
|
const QrCodeDir = "manifest/qrcode"
|
|
|
|
|
|
|
|
|
|
|
|
// AdminEventQrcode 生成活动小程序码:调用微信 getUnlimited → 落盘 → 记录 code_url。
|
|
|
|
|
|
func AdminEventQrcode(ctx context.Context, req *v1.EventQrcodeReq) (*v1.EventQrcodeRes, error) {
|
|
|
|
|
|
event, err := eventRow(ctx, req.EventId)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cfg := xpayConfig(ctx)
|
|
|
|
|
|
if cfg.AppId == "" || cfg.Secret == "" {
|
|
|
|
|
|
return nil, gerror.New("未配置 wx.appId / wx.appSecret,无法生成小程序码;请在该环境 config.yaml 补全后重试")
|
|
|
|
|
|
}
|
|
|
|
|
|
token, err := wxAccessToken(ctx)
|
|
|
|
|
|
if err != nil {
|
2026-10-09 16:49:36 +08:00
|
|
|
|
// 保留底层 errcode/errmsg(gerror.Wrap 而非丢弃),否则「配置没填」与「编码不对」
|
|
|
|
|
|
// 「IP 白名单」「appSecret 错」四种完全不同的问题在后台会显示成同一句话,无法排障。
|
|
|
|
|
|
return nil, gerror.Wrap(err, "获取微信 access_token 失败")
|
2026-09-29 10:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// scene 只承载 event_id(getUnlimited 的 scene 有长度上限,event_id 远不触顶)
|
|
|
|
|
|
scene := strconv.FormatInt(event.Id, 10)
|
|
|
|
|
|
body := g.Map{
|
|
|
|
|
|
"scene": scene,
|
|
|
|
|
|
"page": qrCodePage,
|
|
|
|
|
|
"check_path": false, // 联调期页面可能未发布,避免因校验路径失败
|
|
|
|
|
|
"env_version": "release",
|
|
|
|
|
|
}
|
|
|
|
|
|
resp, err := g.Client().ContentJson().Post(ctx, xpayAPIBase+"/wxa/getwxacodeunlimit?access_token="+token, body)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, gerror.Newf("调用 wxacode.getUnlimited 失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Close()
|
|
|
|
|
|
data := resp.ReadAll()
|
|
|
|
|
|
if len(data) == 0 {
|
|
|
|
|
|
return nil, gerror.New("生成小程序码失败:微信返回空响应")
|
|
|
|
|
|
}
|
|
|
|
|
|
// 成功返回 PNG 二进制;失败返回 JSON(含 errcode/errmsg)
|
|
|
|
|
|
if isJsonBytes(data) {
|
|
|
|
|
|
var we struct {
|
|
|
|
|
|
ErrCode int `json:"errcode"`
|
|
|
|
|
|
ErrMsg string `json:"errmsg"`
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = json.Unmarshal(data, &we)
|
|
|
|
|
|
return nil, gerror.Newf("生成小程序码失败:%d %s", we.ErrCode, we.ErrMsg)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err = gfile.Mkdir(QrCodeDir); err != nil {
|
|
|
|
|
|
return nil, gerror.Newf("创建码图目录失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
fileName := "event_" + scene + "_" + gtime.Now().Format("YmdHis") + ".png"
|
|
|
|
|
|
path := gfile.Join(QrCodeDir, fileName)
|
|
|
|
|
|
if err = gfile.PutBytes(path, data); err != nil {
|
|
|
|
|
|
return nil, gerror.Newf("写入码图失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对外可访问地址(由 cmd.go 的静态路由 /qrcode 提供)
|
|
|
|
|
|
codeUrl := "/qrcode/" + fileName
|
|
|
|
|
|
if _, err = g.Model(consts.TableAnnualEvents).Where("id", event.Id).Data(g.Map{
|
|
|
|
|
|
"code_url": codeUrl,
|
|
|
|
|
|
"updated_at": gtime.Now(),
|
|
|
|
|
|
}).Update(); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &v1.EventQrcodeRes{CodeUrl: codeUrl}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// isJsonBytes 判断响应是否为 JSON(微信失败时返回 JSON,成功返回 PNG)。
|
|
|
|
|
|
func isJsonBytes(b []byte) bool {
|
|
|
|
|
|
t := strings.TrimSpace(string(b))
|
|
|
|
|
|
return strings.HasPrefix(t, "{") || strings.HasPrefix(t, "[")
|
|
|
|
|
|
}
|