服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
71 lines
3.8 KiB
Go
71 lines
3.8 KiB
Go
// 本文件把「持久化能力接口」的名字放在仓储层,供服务层使用。
|
||
//
|
||
// 为什么需要它:投影读缓存(internal/services/dnfbridge/projection_read_cache.go)要在一次扇出里
|
||
// 包住仓储实现,并在转发时保留这些可选能力(字段级保存、批量字段保存、连通性自检、TTL)。
|
||
// 这些接口定义在 internal/platform/db,而 Source Boundaries 的规则 2
|
||
// (internal/architecture/dependency_test.go:99-104)规定服务层不得 import platform/db,
|
||
// 只能经 internal/modules/dnf/repository 访问持久化。仓储层本来就在用这些类型
|
||
// (各仓储接口内嵌 db.Store[Record]),所以由它把名字交给服务层正是这条规则的既定路径。
|
||
//
|
||
// 这里刻意用**类型别名**而不是新定义的接口:别名与原类型完全等价,服务层里的类型断言、
|
||
// 泛型实例化与转发行为一字不变(改前改后语义相同,只是名字换了归属)。
|
||
package repository
|
||
|
||
import (
|
||
"context"
|
||
|
||
"longheng.io/server/internal/platform/db"
|
||
)
|
||
|
||
// Store 是按 key 读写记录的持久化存储。
|
||
type Store[T any] = db.Store[T]
|
||
|
||
// FieldStore 能只保存一条记录的指定字段。
|
||
type FieldStore[T any, F comparable] = db.FieldStore[T, F]
|
||
|
||
// BatchFieldStore 能批量保存多条记录的指定字段。
|
||
type BatchFieldStore[T any, F comparable] = db.BatchFieldStore[T, F]
|
||
|
||
// FieldSave 是一次「记录 + 要保存的字段」。
|
||
type FieldSave[T any, F comparable] = db.FieldSave[T, F]
|
||
|
||
// Checker 是存储的连通性自检能力(仓储健康检查)。
|
||
type Checker = db.Checker
|
||
|
||
// Expirer 是给 key 设置 TTL 的能力。
|
||
type Expirer = db.Expirer
|
||
|
||
// SaveFields 只保存给定字段;normalize 为 nil 时不做字段规整。
|
||
func SaveFields[T any, F comparable](
|
||
ctx context.Context,
|
||
store Store[T],
|
||
record T,
|
||
normalize func([]F) []F,
|
||
fields ...F,
|
||
) error {
|
||
return db.SaveFields(ctx, store, record, normalize, fields...)
|
||
}
|
||
|
||
// 下面的编译期断言钉住「这些名字是**别名**,不是新定义的类型」。
|
||
//
|
||
// 为什么要钉:服务层(projection_read_cache.go)用 `x.(dnfrepo.FieldStore[T, F])` 这样的类型断言
|
||
// 来判断某个仓储实现是否支持字段级保存。别名两边可互相赋值,断言照旧命中;一旦有人把它们改成
|
||
// `type FieldStore[T any, F comparable] interface{ ... }`(新类型),断言就不再命中**仓储实现**,
|
||
// 投影读缓存会静默失效——不报错、不崩溃,只是每次扇出都重新读库(性能回退,且很难发现)。
|
||
// 有了这两组双向赋值断言,那种改动会**编译不过**。
|
||
var (
|
||
_ Store[struct{}] = (db.Store[struct{}])(nil)
|
||
_ db.Store[struct{}] = (Store[struct{}])(nil)
|
||
_ FieldStore[struct{}, int] = (db.FieldStore[struct{}, int])(nil)
|
||
_ db.FieldStore[struct{}, int] = (FieldStore[struct{}, int])(nil)
|
||
_ BatchFieldStore[struct{}, int] = (db.BatchFieldStore[struct{}, int])(nil)
|
||
_ db.BatchFieldStore[struct{}, int] = (BatchFieldStore[struct{}, int])(nil)
|
||
_ Checker = (db.Checker)(nil)
|
||
_ db.Checker = (Checker)(nil)
|
||
_ Expirer = (db.Expirer)(nil)
|
||
_ db.Expirer = (Expirer)(nil)
|
||
_ FieldSave[struct{}, int] = db.FieldSave[struct{}, int]{}
|
||
_ func(context.Context, Store[struct{}], struct{}, func([]int) []int, ...int) error = SaveFields[struct{}, int]
|
||
)
|
||
|