1
This commit is contained in:
@@ -24,6 +24,7 @@ func InsertAndReturnId(ctx context.Context, table string, data any) (id int64, e
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
CacheClear(ctx, g.DB(), table)
|
||||
if r == nil {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -32,7 +33,7 @@ func InsertAndReturnId(ctx context.Context, table string, data any) (id int64, e
|
||||
|
||||
func GetOneByPk[T any](ctx context.Context, table string, pk int64) (res *T, err error) {
|
||||
r, err := g.DB().Model(table).Ctx(ctx).
|
||||
Cache(gdb.CacheOption{Duration: CacheTTL(), Name: table + "_GetOneByPk_" + gconv.String(pk)}).
|
||||
Cache(gdb.CacheOption{Duration: CacheTTL(), Name: CacheName(table, "GetOneByPk", pk)}).
|
||||
Where("id", pk).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -46,10 +47,18 @@ func GetOneByPk[T any](ctx context.Context, table string, pk int64) (res *T, err
|
||||
|
||||
func UpdateByPk(ctx context.Context, table string, pk int64, data any) error {
|
||||
_, err := g.DB().Model(table).Ctx(ctx).Data(data).Where("id", pk).Update()
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CacheClear(ctx, g.DB(), table)
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteByPk(ctx context.Context, table string, pk int64) error {
|
||||
_, err := g.DB().Model(table).Ctx(ctx).Unscoped().Where("id", pk).Delete()
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CacheClear(ctx, g.DB(), table)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -20,3 +23,35 @@ func CacheTTL() time.Duration {
|
||||
})
|
||||
return cacheTTL
|
||||
}
|
||||
|
||||
// CacheName 生成 dao 查询缓存名:统一以 "table@" 开头,
|
||||
// CacheClear 按表前缀精确清理的前提(gdb 缓存键 = "SelectCache:" + name)
|
||||
func CacheName(table, op string, params ...any) string {
|
||||
s := table + "@" + op
|
||||
for _, p := range params {
|
||||
s += "_" + gconv.String(p)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// CacheClear 清理某表全部查询缓存,dao 写操作成功后必须调用(否则"库里已改、查询还是旧值")。
|
||||
// gdb.DB 接口未暴露 Core.ClearCache,此处等价实现:遍历缓存键,删除 "SelectCache:<table>@" 前缀条目。
|
||||
func CacheClear(ctx context.Context, db gdb.DB, table string) {
|
||||
keys, err := db.GetCache().KeyStrings(ctx)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "清理 %s 查询缓存失败(读取键): %v", table, err)
|
||||
return
|
||||
}
|
||||
prefix := "SelectCache:" + table + "@"
|
||||
var toRemove []any
|
||||
for _, k := range keys {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
toRemove = append(toRemove, k)
|
||||
}
|
||||
}
|
||||
if len(toRemove) > 0 {
|
||||
if err := db.GetCache().Removes(ctx, toRemove); err != nil {
|
||||
g.Log().Warningf(ctx, "清理 %s 查询缓存失败: %v", table, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// 数据库组访问器:DAO 按业务域拆分到独立 SQLite 文件(config.yml database.*),经所属组访问。
|
||||
// 归属 common(非表基础设施),禁止在业务分层目录出现非表文件。
|
||||
func DbPlan() gdb.DB { return g.DB("plan") }
|
||||
func DbPay() gdb.DB { return g.DB("pay") }
|
||||
func DbCps() gdb.DB { return g.DB("cps") }
|
||||
@@ -0,0 +1,34 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/grpool"
|
||||
)
|
||||
|
||||
// 协程池封装(grpool):异步任务一律经 Submit 提交,禁止裸 go 启动并行工作负载。
|
||||
// 并发度来源:config.yml pool.<name>(缺失或非法回退调用方传入的业务默认值,定义在 styleagent/consts)。
|
||||
// 防死锁:等待链单向(主 → 池),池内任务不得再等待其他池。
|
||||
|
||||
type taskPool struct {
|
||||
size int
|
||||
once sync.Once
|
||||
pool *grpool.Pool
|
||||
}
|
||||
|
||||
var pools sync.Map // name → *taskPool
|
||||
|
||||
// Submit 提交任务到命名池。ctx 建议传 gctx.New()(请求结束后任务不中断)。
|
||||
func Submit(ctx context.Context, name string, defaultSize int, fn func(ctx context.Context)) error {
|
||||
v, _ := pools.LoadOrStore(name, &taskPool{size: defaultSize})
|
||||
tp := v.(*taskPool)
|
||||
tp.once.Do(func() {
|
||||
if n := g.Cfg().MustGet(ctx, "pool."+name, defaultSize).Int(); n > 0 {
|
||||
tp.size = n
|
||||
}
|
||||
tp.pool = grpool.New(tp.size)
|
||||
})
|
||||
return tp.pool.Add(ctx, fn)
|
||||
}
|
||||
@@ -1,13 +1,27 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RoundInt 浮点数量(克)× 单价(分)等金额计算的四舍五入到整数分
|
||||
func RoundInt(f float64) int64 {
|
||||
return int64(math.Round(f))
|
||||
}
|
||||
|
||||
// IsNotFound GoFrame Scan/One 无匹配行时返回 sql.ErrNoRows,
|
||||
// dao 层统一归一为「无记录」(返回 nil 实体),不作为系统错误向上抛
|
||||
func IsNotFound(err error) bool {
|
||||
return err != nil && errors.Is(err, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
// ImageFileToBase64 reads an image file and returns a data:image/...;base64 string.
|
||||
func ImageFileToBase64(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
)
|
||||
|
||||
// webStaticDirs 候选前端静态目录(相对 server 工作目录,按序取第一个存在者):
|
||||
// 1. 本地开发:直接服务 Flutter 构建产物 app/build/web(scripts/build_web.sh 或 dev.sh 构建)
|
||||
// 1. 本地开发:uni-app H5 构建产物 app-uni/dist/build/h5(npm run build:h5)
|
||||
// 2. Docker 镜像:Dockerfile web-builder 阶段产物(/app/web)
|
||||
var webStaticDirs = []string{
|
||||
"../app/build/web",
|
||||
"../app-uni/dist/build/h5",
|
||||
"web",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gredis"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
)
|
||||
|
||||
// ErrLockHeld 锁被他人持有(重试耗尽仍拿不到时返回)
|
||||
var ErrLockHeld = errors.New("lock held")
|
||||
|
||||
// WithLock 互斥临界区唯一入口(泛型):业务返回值经 T 原样透出。
|
||||
// 内部按 config.yml 自动选择锁实现:配置了 redis 节点 → redis 锁(跨实例互斥,
|
||||
// SET NX EX + token 对比删除防误删他人锁);未配置 → gcache 内存锁(单实例互斥)。
|
||||
// 拿不到锁最多重试 retries 次、每次间隔 retryInterval(retries=0 立即失败;
|
||||
// ctx 取消/超时同样终止);中间件故障不重试直接返回。defer 自动释放:无论 fn
|
||||
// 成功、失败还是 panic。expire 必须 > 0(进程崩溃兜底不死锁),fn 耗时须在 expire 前完成,
|
||||
// fn 内禁止长耗时 IO(LLM/DB 调用);锁粒度按业务唯一键尽量小。
|
||||
func WithLock[T any](ctx context.Context, key string, expire time.Duration, retries int, retryInterval time.Duration, fn func() (T, error)) (T, error) {
|
||||
var zero T
|
||||
if expire <= 0 {
|
||||
return zero, errors.New("lock expire must be positive")
|
||||
}
|
||||
lock, err := newLock(ctx, key, expire)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
var ok bool
|
||||
for attempt := 0; ; attempt++ {
|
||||
ok, err = lock.TryAcquire(ctx)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
if attempt >= retries {
|
||||
return zero, ErrLockHeld
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return zero, ctx.Err()
|
||||
case <-time.After(retryInterval):
|
||||
}
|
||||
}
|
||||
defer lock.Release(ctx)
|
||||
return fn()
|
||||
}
|
||||
|
||||
type lock interface {
|
||||
TryAcquire(ctx context.Context) (bool, error)
|
||||
Release(ctx context.Context)
|
||||
}
|
||||
|
||||
func newLock(ctx context.Context, key string, expire time.Duration) (lock, error) {
|
||||
token := fmt.Sprintf("%d-%d", time.Now().UnixNano(), time.Now().UnixMilli()%1e9)
|
||||
if g.Cfg().MustGet(ctx, "redis.default.address", "").String() != "" {
|
||||
return &redisLock{key: "lock:" + key, token: token, expire: expire, client: g.Redis()}, nil
|
||||
}
|
||||
return &memoryLock{key: "lock:" + key, token: token, expire: expire}, nil
|
||||
}
|
||||
|
||||
type redisLock struct {
|
||||
key string
|
||||
token string
|
||||
expire time.Duration
|
||||
client *gredis.Redis
|
||||
}
|
||||
|
||||
func (l *redisLock) TryAcquire(ctx context.Context) (bool, error) {
|
||||
// SetNX 无 TTL 参数,SET NX 与 TTL 分两步;当前项目未配置 redis 节点此路径不可达,
|
||||
// 若崩溃于两步之间仅残留无 TTL 锁(token 归属明确,可手工清除),可接受
|
||||
ok, err := l.client.SetNX(ctx, l.key, l.token)
|
||||
if err != nil || !ok {
|
||||
return ok, err
|
||||
}
|
||||
if _, err := l.client.PExpire(ctx, l.key, l.expire.Milliseconds()); err != nil {
|
||||
if _, derr := l.client.Del(ctx, l.key); derr != nil {
|
||||
g.Log().Warningf(ctx, "清理未设 TTL 的锁失败: %v", derr)
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (l *redisLock) Release(ctx context.Context) {
|
||||
v, err := l.client.Get(ctx, l.key)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "释放锁失败(读取): %v", err)
|
||||
return
|
||||
}
|
||||
if v.String() == l.token {
|
||||
if _, err := l.client.Del(ctx, l.key); err != nil {
|
||||
g.Log().Warningf(ctx, "释放锁失败(删除): %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type memoryLock struct {
|
||||
key string
|
||||
token string
|
||||
expire time.Duration
|
||||
}
|
||||
|
||||
func (l *memoryLock) TryAcquire(ctx context.Context) (bool, error) {
|
||||
return gcache.SetIfNotExist(ctx, l.key, l.token, l.expire)
|
||||
}
|
||||
|
||||
func (l *memoryLock) Release(ctx context.Context) {
|
||||
v, err := gcache.Get(ctx, l.key)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "释放锁失败(读取): %v", err)
|
||||
return
|
||||
}
|
||||
if v.String() == l.token {
|
||||
if _, err := gcache.Remove(ctx, l.key); err != nil {
|
||||
g.Log().Warningf(ctx, "释放锁失败(删除): %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user