Files
qitongxue-api/internal/logic/event_code.go
李琦 2bce19b05b 小程序码落图片库并返回绝对地址
码图原先落在 qrcode 私有目录且返回相对路径,后台无法统一管理、
换部署域名还要改数据。现迁到图片库 manifest/static/qrcode/,
与头像/封面/附件共用同一套 /static 路由,并登记附件表
(scene=qrcode)使后台附件管理可统一查看管理。

库内仍只存相对路径、域名由 absoluteURL 按配置统一拼,与既有
约定一致;按内容 md5 命名,重复生成同一张码不新增文件。
活动码与项目绑定码共用 saveQrcodeImage helper。

另修正 .gitignore 漏掉的仓库根二进制(go build -o 的产物)。
2026-10-13 16:23:21 +08:00

136 lines
6.1 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 {
// 保留底层 errcode/errmsg(gerror.Wrap 而非丢弃),否则「配置没填」与「编码不对」
// 「IP 白名单」「appSecret 错」四种完全不同的问题在后台会显示成同一句话,无法排障。
return nil, gerror.Wrap(err, "获取微信 access_token 失败")
}
// 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)
}
// 🔴 落「图片库」而不是 qrcode 私有目录(迭代-2026-10-09):用户要求「小程序码能上传到
// 图片库、并返回上传后的地址」。图片库 = manifest/static/(与头像/封面/附件同一套,
// 由 /static 路由对外),落进去后台「附件管理」能统一看到/管理这些码图。
relPath, absURL, err := saveQrcodeImage(ctx, "event_"+scene+".png", data)
if err != nil {
return nil, err
}
// 对外返回**绝对 URL**(浏览器/后台可直接打开或下载)。
// 库内仍只存相对路径(/static/...),域名由 absoluteURL 按 settings.site.base_url 统一拼 ——
// 与头像/封面保持同一套约定,换域名不用改数据。
if _, err = g.Model(consts.TableAnnualEvents).Where("id", event.Id).Data(g.Map{
"code_url": relPath,
"updated_at": gtime.Now(),
}).Update(); err != nil {
return nil, err
}
return &v1.EventQrcodeRes{CodeUrl: absURL}, nil
}
// saveQrcodeImage 把小程序码二进制落进「图片库」并登记附件表。
//
// 🔴 为什么落图片库而不是 qrcode 私有目录(迭代-2026-10-09):用户要求码图「能上传到图片库、
// 返回上传后的地址」。图片库 = manifest/static/(与头像/封面/附件同一套,由 /static 路由对外),
// 落进去后台「附件管理」能统一看到并管理这些码图。
//
//去重:storeStatic 内部按内容 md5 命名 → 同一张码重复生成不会在图片库里堆垃圾
// (微信对同一 scene/page 返回的码图是稳定字节,md5 一致即覆盖同一文件)。
//
// 返回:库内相对路径(/static/...)+ 对外绝对 URL。**库内一律存相对路径**,域名只在
// absoluteURL 里出现(与头像/封面同一约定),换部署域名时不用改数据。
func saveQrcodeImage(ctx context.Context, fileName string, data []byte) (relPath string, absURL string, err error) {
// 兜底目录:/qrcode 静态路由在 cmd.go 注册时要求目录已存在(AddStaticPath 目录不存在会 FATAL)。
// 这里继续保证它存在,避免 cmd.go 之外的调用路径(如单测)漏建。
if err = gfile.Mkdir(QrCodeDir); err != nil {
g.Log().Errorf(ctx, "[qrcode] 创建码图目录失败 dir=%s err=%v", QrCodeDir, err)
}
relPath, err = storeStatic(ctx, consts.AttachmentSceneQrcode, fileName, data)
if err != nil {
return "", "", err
}
recordAttachment(ctx, consts.AttachmentSceneQrcode, fileName, relPath, data, "admin", CtxAdminId(ctx))
return relPath, absoluteURL(ctx, relPath), nil
}
// isJsonBytes 判断响应是否为 JSON(微信失败时返回 JSON,成功返回 PNG)。
func isJsonBytes(b []byte) bool {
t := strings.TrimSpace(string(b))
return strings.HasPrefix(t, "{") || strings.HasPrefix(t, "[")
}