小程序一些功能更新

This commit is contained in:
李琦
2026-09-29 10:57:01 +08:00
parent 0cb70a7618
commit 3bb7fa8db3
96 changed files with 13890 additions and 213 deletions

View File

@@ -4,15 +4,30 @@ import (
"context"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/gcron"
"github.com/gogf/gf/v2/os/gfile"
"tool-api/internal/controller"
"tool-api/internal/logic"
)
// GoFrame gcron 的 pattern 是 **6 段**(秒 分 时 日 月 周),不是标准 crontab 的 5 段。
//
// ⚠️ 写成 5 段既不会 panic 也不会启动失败,只会让 AddSingleton 返回 error 并被下面的
// WARNING 吞掉 —— 任务静默不注册、功能完全失效(本项目曾把「笔记提醒扫描」写成
// "*/1 * * * *" 而无人察觉)。故把 pattern 提为常量,并由 cmd_test.go 固化校验。
// 若确实要用标准 5 段 crontab,需前置一个 `#`(如 "# */1 * * * *")声明忽略秒。
const (
cronPatternSweepOrders = "0 */5 * * * *" // 会员订单扫单:每 5 分钟
cronPatternScanRemind = "0 */1 * * * *" // 笔记提醒扫描:每 1 分钟
cronPatternReleaseCoupons = "0 */5 * * * *" // 免单券超时占用释放:每 5 分钟
cronPatternCloseOrders = "0 */5 * * * *" // 订单超时关闭:每 5 分钟
)
var (
Main = gcmd.Command{
Name: "main",
@@ -25,30 +40,77 @@ var (
logic.Seed(ctx)
// 会员订单兜底扫单:推送丢失时主动查单补发货(建议间隔 5 分钟)
if _, err = gcron.AddSingleton(ctx, "0 */5 * * * *", func(c context.Context) {
if _, err = gcron.AddSingleton(ctx, cronPatternSweepOrders, func(c context.Context) {
logic.SweepPendingOrders(c)
}, "member-order-sweep"); err != nil {
g.Log().Warningf(ctx, "[cron] 会员订单扫单任务注册失败: %v", err)
} else {
g.Log().Infof(ctx, "[cron] 已注册任务 %s pattern=%q", "member-order-sweep", cronPatternSweepOrders)
}
// 笔记定时提醒扫描:每分钟扫描到点的提醒并发订阅消息(条件更新保证只发一次)
if _, err = gcron.AddSingleton(ctx, cronPatternScanRemind, func(c context.Context) {
logic.ScanDueReminders(c)
}, "note-remind-scan"); err != nil {
g.Log().Warningf(ctx, "[cron] 笔记提醒扫描任务注册失败: %v", err)
} else {
g.Log().Infof(ctx, "[cron] 已注册任务 %s pattern=%q", "note-remind-scan", cronPatternScanRemind)
}
// 免单券超时占用释放:订单取消/支付失败/超时(coupon.lock.ttlSeconds)后把券释放回可用
if _, err = gcron.AddSingleton(ctx, cronPatternReleaseCoupons, func(c context.Context) {
logic.ReleaseExpiredCouponLocks(c)
}, "coupon-lock-release"); err != nil {
g.Log().Warningf(ctx, "[cron] 免单券释放任务注册失败: %v", err)
} else {
g.Log().Infof(ctx, "[cron] 已注册任务 %s pattern=%q", "coupon-lock-release", cronPatternReleaseCoupons)
}
// 订单超时关闭:把超时未支付的待支付单置为已关闭(status=3)并释放其占用中的券。
// 时限 orderPendingTtlSeconds 与上面「免单券释放」同源(同一 settings 键,D2)。
if _, err = gcron.AddSingleton(ctx, cronPatternCloseOrders, func(c context.Context) {
logic.ClosePendingOrders(c)
}, "order-close-pending"); err != nil {
g.Log().Warningf(ctx, "[cron] 订单超时关闭任务注册失败: %v", err)
} else {
g.Log().Infof(ctx, "[cron] 已注册任务 %s pattern=%q", "order-close-pending", cronPatternCloseOrders)
}
s := g.Server()
// 公开接口:小程序登录、开发登录、管理端登录、虚拟支付发货推送回调
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(logic.CORS, logic.Response)
group.Bind(controller.UserPub, controller.AdminPub)
group.Bind(controller.UserPub, controller.AdminPub, controller.EventPub)
// 平台回调无 token,单独注册(响应体为 XML,不走统一 JSON 包装)
group.POST("/pay/notify", controller.PayNotify)
})
// 小程序用户接口
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(logic.CORS, logic.Response, logic.UserAuth)
group.Bind(controller.UserAuth, controller.Member, controller.Quota)
group.Bind(controller.UserAuth, controller.Member, controller.Quota, controller.Note,
controller.Promo, controller.Coupon)
})
// 管理端接口
// 管理端接口:鉴权(AdminAuth)+ 权限分级(AdminPerm,按角色权限点放行)
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(logic.CORS, logic.Response, logic.AdminAuth)
group.Bind(controller.AdminAuth)
group.Middleware(logic.CORS, logic.Response, logic.AdminAuth, logic.AdminPerm)
group.Bind(controller.AdminAuth, controller.Event, controller.AdminPromo, controller.AdminCoupon, controller.AdminOrder)
})
// 活动小程序码静态资源(生成时落在 manifest/qrcode,供管理端/前端访问)
// ⚠️ AddStaticPath 在目录不存在时**直接 FATAL 退出、不降级**,而该目录原先只在
// 生成码图时才惰性创建 → 首次启动必然崩在「目录还没被创建」上(先有鸡先有蛋)。
// 故此处先幂等建目录(gfile.Mkdir = os.MkdirAll,已存在返回 nil)再注册。
if err = gfile.Mkdir(logic.QrCodeDir); err != nil {
return gerror.Wrapf(err, "预创建码图目录 %s 失败(静态路由 /qrcode 无法注册)", logic.QrCodeDir)
}
s.AddStaticPath("/qrcode", logic.QrCodeDir)
// 用户头像静态资源(N3):落盘于 manifest/static/avatar/...,由 /static 提供访问。
// 与 /qrcode 同一套坑与治法:先幂等建目录再注册静态路由(AddStaticPath 目录不存在会 FATAL)。
if err = gfile.Mkdir(logic.StaticDir); err != nil {
return gerror.Wrapf(err, "预创建静态目录 %s 失败(静态路由 /static 无法注册)", logic.StaticDir)
}
s.AddStaticPath("/static", logic.StaticDir)
s.Run()
return nil
},