Files
qitongxue-api/internal/logic/event_code.go
2026-09-29 10:57:01 +08:00

110 lines
4.2 KiB
Go
Raw 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 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 {
// 把底层错误转成人话,不把原始英文/错误码直接抛给用户
return nil, gerror.New("获取微信 access_token 失败,请稍后重试或检查 wx.appId / wx.appSecret 配置")
}
// 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, "[")
}