或就爱你功能

This commit is contained in:
zhx 2026-06-10 16:41:13 +08:00
parent f5842f8702
commit 7b2b3a3e2f
6 changed files with 52 additions and 4 deletions

View File

@ -85,7 +85,7 @@ go run ./cmd/server -config configs/config.yaml -bootstrap
5. 新增任务类型时需要补齐创建接口、runner handler、重试/失败结果和产物策略。
6. 区域创建和区域启用/禁用是后台管理能力,实现在 `hyapp-admin-server`,不要为此新增 user-service RPC 或把后台开关逻辑下沉到 app 后端服务。
7. 国家创建和国家启用/禁用是后台管理后端能力,实现在 `hyapp-admin-server`,不要为这些后台动作新增 user-service RPC 或 user-service 业务逻辑。
8. 后台创建 BD Leader 必须传后台选择的 `regionId`admin-server 负责权限和审计user-service 在关系创建事务里更新目标用户 `users.region_id`,不要在 admin-server 里用 GetUser 做“用户原区域必须一致”的拒绝
8. 后台创建 BD Leader 按目标用户当前 `users.region_id` 创建admin-server 只负责权限和审计,不传 `regionId`user-service 在关系创建事务里读取目标用户区域并写入 `bd_leader_profiles.region_id`,不要在创建 BD Leader 时顺手修改用户区域
9. `hyapp.local/api` 是唯一允许直接依赖的 app 后端契约 module不要 import `hyapp/services/*``hyapp/internal/*``hyapp/pkg/*`
## 验证

View File

@ -143,7 +143,12 @@ func (h *Handler) SetBDStatus(c *gin.Context) {
}
func bdRoleFromStatusRoute(c *gin.Context) (string, string) {
if strings.HasPrefix(c.FullPath(), "/admin/bd-leaders/") {
return bdRoleFromStatusPath(c.FullPath())
}
func bdRoleFromStatusPath(fullPath string) (string, string) {
// FullPath 带有外层 /api/v1 分组;只要命中 BD Leader 状态路由片段,就必须走独立的 bd_leader_profiles避免误查普通 bd_profiles。
if strings.Contains(fullPath, "/admin/bd-leaders/") {
return "bd_leader", "bd_leader_profiles"
}
return "bd", "bd_profiles"

View File

@ -12,6 +12,19 @@ func TestNormalizeListQueryAllowsCoinSellerComputedSorts(t *testing.T) {
}
}
// TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix 锁定线上真实 FullPath 带 /api/v1 前缀时BD Leader 状态仍然写独立负责人表。
func TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix(t *testing.T) {
role, target := bdRoleFromStatusPath("/api/v1/admin/bd-leaders/:user_id/status")
if role != "bd_leader" || target != "bd_leader_profiles" {
t.Fatalf("leader status route must use bd_leader_profiles, got role=%q target=%q", role, target)
}
role, target = bdRoleFromStatusPath("/api/v1/admin/bds/:user_id/status")
if role != "bd" || target != "bd_profiles" {
t.Fatalf("bd status route must use bd_profiles, got role=%q target=%q", role, target)
}
}
// TestSortCoinSellerListItemsComputedFields 验证 wallet 聚合字段排序只改变展示顺序,不改动币商身份数据。
func TestSortCoinSellerListItemsComputedFields(t *testing.T) {
items := []*CoinSellerListItem{

View File

@ -496,6 +496,7 @@ func rocketStateForNow(input *state.RocketState, cfg RoomRocketConfig, now time.
rocket.CurrentFuel = rocket.FuelThreshold
}
rocket.PendingLaunches = validRocketPendingLaunches(rocket.PendingLaunches, rocket.ResetAtMS)
syncRocketLaunchSnapshotFromPending(rocket)
if rocket.Status == state.RocketStatusCompleted {
rocket.CurrentLevel = roomRocketLevelCount
rocket.FuelThreshold = roomRocketLevelByNumber(cfg, roomRocketLevelCount).FuelThreshold
@ -510,6 +511,31 @@ func rocketStateForNow(input *state.RocketState, cfg RoomRocketConfig, now time.
return rocket
}
func syncRocketLaunchSnapshotFromPending(rocket *state.RocketState) {
if rocket == nil {
return
}
if len(rocket.PendingLaunches) == 0 {
// 当前充能档没有待发射火箭时,不能继续暴露上一枚火箭的发射时间,否则客户端会把过期 launch_at_ms 渲染成 00:00:00。
rocket.IgnitedAtMS = 0
rocket.LaunchAtMS = 0
rocket.Top1UserID = 0
rocket.IgniterUserID = 0
return
}
next := rocket.PendingLaunches[0]
for _, launch := range rocket.PendingLaunches[1:] {
if launch.LaunchAtMS > 0 && (next.LaunchAtMS <= 0 || launch.LaunchAtMS < next.LaunchAtMS) {
next = launch
}
}
// RoomRocketState 上的倒计时字段兼容旧客户端;真实多枚队列仍以 PendingLaunches 为准。
rocket.IgnitedAtMS = next.IgnitedAtMS
rocket.LaunchAtMS = next.LaunchAtMS
rocket.Top1UserID = next.Top1UserID
rocket.IgniterUserID = next.IgniterUserID
}
func validRocketPendingLaunches(input []state.RocketPendingLaunch, resetAtMS int64) []state.RocketPendingLaunch {
out := make([]state.RocketPendingLaunch, 0, len(input))
seen := make(map[string]bool, len(input))
@ -821,6 +847,7 @@ func (s *Service) launchRoomRocket(ctx context.Context, cmd command.LaunchRoomRo
pendingAfter := removeRocketPendingLaunch(current.Rocket.PendingLaunches, pendingIndex)
current.Rocket.PendingLaunches = pendingAfter
syncRocketLaunchSnapshotFromPending(current.Rocket)
current.Rocket.LaunchedAtMS = nowMS
current.Rocket.LastRewards = rewards

View File

@ -425,6 +425,9 @@ func TestRoomRocketLaunchGrantsLockedTop1AndIgniterAfterLeave(t *testing.T) {
if stateView.GetCurrentLevel() != 2 || stateView.GetStatus() != state.RocketStatusCharging || len(stateView.GetPendingLaunches()) != 0 {
t.Fatalf("launch rocket must keep current next-level progress and clear pending launch: %+v", stateView)
}
if stateView.GetLaunchAtMs() != 0 || stateView.GetIgnitedAtMs() != 0 || stateView.GetTop1UserId() != 0 || stateView.GetIgniterUserId() != 0 {
t.Fatalf("launched rocket must not leak expired launch countdown into next charging level: %+v", stateView)
}
if len(stateView.GetLastRewards()) != 3 {
t.Fatalf("launch rocket must retain in-room, top1 and igniter grants for client display: %+v", stateView.GetLastRewards())
}

View File

@ -181,13 +181,13 @@ type RocketState struct {
FuelThreshold int64
// Status 区分 charging/completed已点火火箭在 PendingLaunches 中独立排队,不阻塞当前进度。
Status string
// IgnitedAtMS/LaunchAtMS 保留最近一次点火快照,客户端可用 PendingLaunches 获得完整队列
// IgnitedAtMS/LaunchAtMS 投影下一枚待发射火箭,完整多枚队列仍以 PendingLaunches 为准
IgnitedAtMS int64
LaunchAtMS int64
LaunchedAtMS int64
// ResetAtMS 是下一次 UTC 自然日重置毫秒时间。
ResetAtMS int64
// Top1UserID 和 IgniterUserID 在燃料满瞬间锁定,用户离房或下线仍按这里发奖
// Top1UserID 和 IgniterUserID 投影下一枚待发射火箭的锁定用户;发奖以 PendingLaunches 内快照为准
Top1UserID int64
IgniterUserID int64
// RocketID 是单次火箭从积攒到发射的幂等标识,奖励抽取和 wallet command_id 都依赖它。