2026-07-02 16:55:34 +08:00
|
|
|
|
// Package modules - 渡劫系统模块
|
|
|
|
|
|
// 对齐GDD-12 渡劫破镜与境界掉落系统 + T005审阅报告
|
|
|
|
|
|
package modules
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"database/sql"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"math/rand"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/heroiclabs/nakama-common/runtime"
|
|
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// RegisterTribulation 注册渡劫相关 RPC。
|
|
|
|
|
|
func RegisterTribulation(initializer runtime.Initializer) error {
|
2026-07-03 21:34:51 +08:00
|
|
|
|
rpcs := map[string]func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error){
|
2026-07-02 16:55:34 +08:00
|
|
|
|
"TribulationService/PrepareTribulation": prepareTribulation,
|
2026-07-03 21:34:51 +08:00
|
|
|
|
"TribulationService/StartTribulation": tribulationStart,
|
2026-07-02 16:55:34 +08:00
|
|
|
|
"TribulationService/GetTribulationInfo": getTribulationInfo,
|
|
|
|
|
|
"TribulationService/InviteHelper": inviteHelper,
|
|
|
|
|
|
}
|
|
|
|
|
|
for path, fn := range rpcs {
|
|
|
|
|
|
if err := initializer.RegisterRpc(path, fn); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- 请求/响应结构 ---
|
|
|
|
|
|
|
|
|
|
|
|
type prepareTribulationReq struct {
|
|
|
|
|
|
CharacterID string `json:"character_id"`
|
|
|
|
|
|
TargetRealm int32 `json:"target_realm"` // 目标境界tier
|
|
|
|
|
|
TargetMinor int32 `json:"target_minor"` // 目标小境界
|
|
|
|
|
|
Pills []string `json:"pills"` // 使用的丹药ID
|
|
|
|
|
|
Artifacts []string `json:"artifacts"` // 使用的法宝ID
|
|
|
|
|
|
Helpers []string `json:"helpers"` // 护法玩家ID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type startTribulationReq struct {
|
|
|
|
|
|
CharacterID string `json:"character_id"`
|
|
|
|
|
|
SessionType string `json:"session_type"` // manual/auto
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type getTribulationInfoReq struct {
|
|
|
|
|
|
CharacterID string `json:"character_id"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type inviteHelperReq struct {
|
|
|
|
|
|
CharacterID string `json:"character_id"`
|
|
|
|
|
|
HelperID string `json:"helper_id"` // 被邀请玩家ID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type tribulationInfoData struct {
|
|
|
|
|
|
CharacterID string `json:"character_id"`
|
|
|
|
|
|
CurrentRealm int32 `json:"current_realm"`
|
|
|
|
|
|
CurrentMinor int32 `json:"current_minor"`
|
|
|
|
|
|
TargetRealm int32 `json:"target_realm"`
|
|
|
|
|
|
TargetMinor int32 `json:"target_minor"`
|
|
|
|
|
|
BaseSuccessRate float64 `json:"base_success_rate"`
|
|
|
|
|
|
ModifiedRate float64 `json:"modified_rate"`
|
|
|
|
|
|
TribulationType string `json:"tribulation_type"` // thunder/fire/heart_devil/human/earth/life/chaos
|
|
|
|
|
|
Helpers []string `json:"helpers"`
|
|
|
|
|
|
Pills []string `json:"pills"`
|
|
|
|
|
|
Artifacts []string `json:"artifacts"`
|
|
|
|
|
|
IsMajorBreakthrough bool `json:"is_major_breakthrough"`
|
|
|
|
|
|
PreparationStatus string `json:"preparation_status"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- 渡劫类型判定 ---
|
|
|
|
|
|
|
|
|
|
|
|
func determineTribulationType(realmTier int32, karmaValue int32, sinValue int32, sanValue int32) string {
|
|
|
|
|
|
// 大境界突破(3的倍数)触发三劫连环
|
|
|
|
|
|
if realmTier%3 == 0 && realmTier > 1 {
|
|
|
|
|
|
return "triple" // 雷罚→业火→心魔
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 高罪孽触发天罚天劫
|
|
|
|
|
|
if sinValue >= 500 {
|
|
|
|
|
|
return "heavenly_punishment"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 低SAN触发旧日注视(克苏鲁系)
|
|
|
|
|
|
if sanValue < 30 {
|
|
|
|
|
|
return "old_gaze"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 默认雷罚天劫
|
|
|
|
|
|
return "thunder"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func calculateSuccessRate(baseRate float64, pills []string, artifacts []string, karmaValue int32, sinValue int32, dantoxLevel int32, purity float64) float64 {
|
|
|
|
|
|
modified := baseRate
|
|
|
|
|
|
|
|
|
|
|
|
// 丹药加成
|
|
|
|
|
|
modified += float64(len(pills)) * 0.05
|
|
|
|
|
|
|
|
|
|
|
|
// 法宝加成
|
|
|
|
|
|
modified += float64(len(artifacts)) * 0.03
|
|
|
|
|
|
|
|
|
|
|
|
// 功德加成
|
|
|
|
|
|
if karmaValue >= 1000 {
|
|
|
|
|
|
modified += 0.10
|
|
|
|
|
|
} else if karmaValue >= 500 {
|
|
|
|
|
|
modified += 0.05
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 罪孽惩罚
|
|
|
|
|
|
if sinValue >= 2000 {
|
|
|
|
|
|
modified -= 0.30
|
|
|
|
|
|
} else if sinValue >= 500 {
|
|
|
|
|
|
modified -= 0.15
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 丹毒惩罚
|
|
|
|
|
|
if dantoxLevel > 100 {
|
|
|
|
|
|
modified -= 0.15
|
|
|
|
|
|
} else if dantoxLevel > 60 {
|
|
|
|
|
|
modified -= 0.08
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 纯净度加成
|
|
|
|
|
|
if purity >= 0.9 {
|
|
|
|
|
|
modified += 0.15
|
|
|
|
|
|
} else if purity >= 0.7 {
|
|
|
|
|
|
modified += 0.08
|
|
|
|
|
|
} else if purity < 0.3 {
|
|
|
|
|
|
modified -= 0.15
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 限制范围
|
|
|
|
|
|
if modified < 0.05 {
|
|
|
|
|
|
modified = 0.05
|
|
|
|
|
|
}
|
|
|
|
|
|
if modified > 0.95 {
|
|
|
|
|
|
modified = 0.95
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return modified
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- RPC 实现 ---
|
|
|
|
|
|
|
|
|
|
|
|
func prepareTribulation(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
|
|
|
|
|
traceID := newTraceID()
|
|
|
|
|
|
uid := userIDFromCtx(ctx)
|
|
|
|
|
|
if uid == "" {
|
|
|
|
|
|
return errResp(1001, "missing token", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req prepareTribulationReq
|
|
|
|
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|
|
|
|
|
return errResp(2001, "invalid payload", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询角色信息
|
|
|
|
|
|
var charInfo struct {
|
|
|
|
|
|
ID string
|
|
|
|
|
|
RealmTier int32
|
|
|
|
|
|
MinorRealm int32
|
|
|
|
|
|
KarmaValue int32
|
|
|
|
|
|
SinValue int32
|
|
|
|
|
|
DantoxLevel int32
|
|
|
|
|
|
Purity float64
|
|
|
|
|
|
SanValue int32
|
|
|
|
|
|
}
|
|
|
|
|
|
err := hhdbPool.QueryRow(ctx, `
|
|
|
|
|
|
SELECT id, realm_tier, minor_realm, karma_value, crime_score, dantox_level, energy_purity, san_current
|
|
|
|
|
|
FROM characters WHERE id = $1 AND player_id = $2
|
|
|
|
|
|
`, req.CharacterID, uid).Scan(
|
|
|
|
|
|
&charInfo.ID, &charInfo.RealmTier, &charInfo.MinorRealm,
|
|
|
|
|
|
&charInfo.KarmaValue, &charInfo.SinValue, &charInfo.DantoxLevel,
|
|
|
|
|
|
&charInfo.Purity, &charInfo.SanValue,
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errResp(4002, "character not found", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否可渡劫
|
|
|
|
|
|
if charInfo.RealmTier >= 9 {
|
|
|
|
|
|
return errResp(5010, "already at max realm", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确定渡劫类型
|
|
|
|
|
|
tribType := determineTribulationType(charInfo.RealmTier, charInfo.KarmaValue, charInfo.SinValue, charInfo.SanValue)
|
|
|
|
|
|
|
|
|
|
|
|
// 计算成功率
|
|
|
|
|
|
var realmInfo struct {
|
|
|
|
|
|
BaseSuccessRate float64
|
|
|
|
|
|
}
|
|
|
|
|
|
err = hhdbPool.QueryRow(ctx, `
|
|
|
|
|
|
SELECT base_success_rate FROM realms WHERE tier = $1
|
|
|
|
|
|
`, charInfo.RealmTier).Scan(&realmInfo.BaseSuccessRate)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
realmInfo.BaseSuccessRate = 0.50
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modifiedRate := calculateSuccessRate(
|
|
|
|
|
|
realmInfo.BaseSuccessRate,
|
|
|
|
|
|
req.Pills, req.Artifacts,
|
|
|
|
|
|
charInfo.KarmaValue, charInfo.SinValue,
|
|
|
|
|
|
charInfo.DantoxLevel, charInfo.Purity,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
isMajor := charInfo.RealmTier%3 == 0
|
|
|
|
|
|
|
|
|
|
|
|
return okResp(tribulationInfoData{
|
|
|
|
|
|
CharacterID: charInfo.ID,
|
|
|
|
|
|
CurrentRealm: charInfo.RealmTier,
|
|
|
|
|
|
CurrentMinor: charInfo.MinorRealm,
|
|
|
|
|
|
TargetRealm: req.TargetRealm,
|
|
|
|
|
|
TargetMinor: req.TargetMinor,
|
|
|
|
|
|
BaseSuccessRate: realmInfo.BaseSuccessRate,
|
|
|
|
|
|
ModifiedRate: modifiedRate,
|
|
|
|
|
|
TribulationType: tribType,
|
|
|
|
|
|
Helpers: req.Helpers,
|
|
|
|
|
|
Pills: req.Pills,
|
|
|
|
|
|
Artifacts: req.Artifacts,
|
|
|
|
|
|
IsMajorBreakthrough: isMajor,
|
|
|
|
|
|
PreparationStatus: "ready",
|
|
|
|
|
|
}, traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 21:34:51 +08:00
|
|
|
|
func tribulationStart(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
2026-07-02 16:55:34 +08:00
|
|
|
|
traceID := newTraceID()
|
|
|
|
|
|
uid := userIDFromCtx(ctx)
|
|
|
|
|
|
if uid == "" {
|
|
|
|
|
|
return errResp(1001, "missing token", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req startTribulationReq
|
|
|
|
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|
|
|
|
|
return errResp(2001, "invalid payload", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询角色信息
|
|
|
|
|
|
var charID string
|
|
|
|
|
|
var realmTier, minorRealm int32
|
|
|
|
|
|
var karmaValue, sinValue, dantoxLevel, sanCurrent int32
|
|
|
|
|
|
var purity float64
|
|
|
|
|
|
err := hhdbPool.QueryRow(ctx, `
|
|
|
|
|
|
SELECT id, realm_tier, minor_realm, karma_value, crime_score, dantox_level, energy_purity, san_current
|
|
|
|
|
|
FROM characters WHERE id = $1 AND player_id = $2
|
|
|
|
|
|
`, req.CharacterID, uid).Scan(
|
|
|
|
|
|
&charID, &realmTier, &minorRealm,
|
|
|
|
|
|
&karmaValue, &sinValue, &dantoxLevel,
|
|
|
|
|
|
&purity, &sanCurrent,
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errResp(4002, "character not found", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确定渡劫类型
|
|
|
|
|
|
tribType := determineTribulationType(realmTier, karmaValue, sinValue, sanCurrent)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取基础成功率
|
|
|
|
|
|
var baseRate float64
|
|
|
|
|
|
err = hhdbPool.QueryRow(ctx, `SELECT base_success_rate FROM realms WHERE tier = $1`, realmTier).Scan(&baseRate)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
baseRate = 0.50
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modifiedRate := calculateSuccessRate(baseRate, nil, nil, karmaValue, sinValue, dantoxLevel, purity)
|
|
|
|
|
|
|
|
|
|
|
|
// 判定渡劫结果
|
|
|
|
|
|
success := rand.Float64() < modifiedRate
|
|
|
|
|
|
|
|
|
|
|
|
var result string
|
|
|
|
|
|
var penalties map[string]interface{}
|
|
|
|
|
|
|
|
|
|
|
|
if success {
|
|
|
|
|
|
result = "success"
|
|
|
|
|
|
// 成功:境界提升
|
|
|
|
|
|
newMinor := minorRealm + 1
|
|
|
|
|
|
newRealm := realmTier
|
|
|
|
|
|
if newMinor > 3 {
|
|
|
|
|
|
newMinor = 1
|
|
|
|
|
|
newRealm = realmTier + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_, err = hhdbPool.Exec(ctx, `
|
|
|
|
|
|
UPDATE characters SET realm_tier = $1, minor_realm = $2, updated_at = NOW()
|
|
|
|
|
|
WHERE id = $3
|
|
|
|
|
|
`, newRealm, newMinor, charID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Error("tribulation success update failed: %v", err)
|
|
|
|
|
|
return errResp(9002, "internal error", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
penalties = map[string]interface{}{
|
|
|
|
|
|
"realm_tier": newRealm,
|
|
|
|
|
|
"minor_realm": newMinor,
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result = "fail"
|
|
|
|
|
|
// 失败:境界掉落
|
|
|
|
|
|
newMinor := minorRealm - 1
|
|
|
|
|
|
newRealm := realmTier
|
|
|
|
|
|
if newMinor < 1 {
|
|
|
|
|
|
newMinor = 3
|
|
|
|
|
|
newRealm = realmTier - 1
|
|
|
|
|
|
if newRealm < 1 {
|
|
|
|
|
|
newRealm = 1
|
|
|
|
|
|
newMinor = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_, err = hhdbPool.Exec(ctx, `
|
|
|
|
|
|
UPDATE characters SET realm_tier = $1, minor_realm = $2, updated_at = NOW()
|
|
|
|
|
|
WHERE id = $3
|
|
|
|
|
|
`, newRealm, newMinor, charID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Error("tribulation fail update failed: %v", err)
|
|
|
|
|
|
return errResp(9002, "internal error", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
penalties = map[string]interface{}{
|
|
|
|
|
|
"realm_tier": newRealm,
|
|
|
|
|
|
"minor_realm": newMinor,
|
|
|
|
|
|
"debuff": "天劫余威:全属性-10%,持续6-24游戏时",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 记录渡劫记录
|
|
|
|
|
|
_, err = hhdbPool.Exec(ctx, `
|
|
|
|
|
|
INSERT INTO tribulation_records
|
|
|
|
|
|
(character_id, realm_tier, minor_realm, tribulation_type, base_success_rate, modified_success_rate, result, penalties, san_snapshot, created_at)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
|
|
|
|
|
|
`, charID, realmTier, minorRealm, tribType, baseRate, modifiedRate, result, penalties, sanCurrent)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Error("tribulation record insert failed: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return okResp(map[string]interface{}{
|
|
|
|
|
|
"success": success,
|
|
|
|
|
|
"result": result,
|
|
|
|
|
|
"tribulation_type": tribType,
|
|
|
|
|
|
"base_rate": baseRate,
|
|
|
|
|
|
"modified_rate": modifiedRate,
|
|
|
|
|
|
"penalties": penalties,
|
|
|
|
|
|
}, traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getTribulationInfo(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
|
|
|
|
|
traceID := newTraceID()
|
|
|
|
|
|
uid := userIDFromCtx(ctx)
|
|
|
|
|
|
if uid == "" {
|
|
|
|
|
|
return errResp(1001, "missing token", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req getTribulationInfoReq
|
|
|
|
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|
|
|
|
|
return errResp(2001, "invalid payload", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询最近的渡劫记录
|
|
|
|
|
|
var record struct {
|
|
|
|
|
|
TribulationType string
|
|
|
|
|
|
BaseRate float64
|
|
|
|
|
|
ModifiedRate float64
|
|
|
|
|
|
Result string
|
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
|
}
|
|
|
|
|
|
err := hhdbPool.QueryRow(ctx, `
|
|
|
|
|
|
SELECT tribulation_type, base_success_rate, modified_success_rate, result, created_at
|
|
|
|
|
|
FROM tribulation_records
|
|
|
|
|
|
WHERE character_id = $1
|
|
|
|
|
|
ORDER BY created_at DESC LIMIT 1
|
|
|
|
|
|
`, req.CharacterID).Scan(
|
|
|
|
|
|
&record.TribulationType, &record.BaseRate,
|
|
|
|
|
|
&record.ModifiedRate, &record.Result, &record.CreatedAt,
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil && err != pgx.ErrNoRows {
|
|
|
|
|
|
logger.Error("get tribulation info failed: %v", err)
|
|
|
|
|
|
return errResp(9002, "internal error", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return okResp(map[string]interface{}{
|
|
|
|
|
|
"character_id": req.CharacterID,
|
|
|
|
|
|
"last_tribulation": record,
|
|
|
|
|
|
}, traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func inviteHelper(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
|
|
|
|
|
traceID := newTraceID()
|
|
|
|
|
|
uid := userIDFromCtx(ctx)
|
|
|
|
|
|
if uid == "" {
|
|
|
|
|
|
return errResp(1001, "missing token", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req inviteHelperReq
|
|
|
|
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|
|
|
|
|
return errResp(2001, "invalid payload", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证被邀请者存在
|
|
|
|
|
|
var helperName string
|
|
|
|
|
|
err := hhdbPool.QueryRow(ctx, `
|
|
|
|
|
|
SELECT name FROM characters WHERE id = $1 AND status = 'active'
|
|
|
|
|
|
`, req.HelperID).Scan(&helperName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errResp(5001, "helper not found", traceID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送邀请通知(通过Nakama通知系统)
|
2026-07-03 21:34:51 +08:00
|
|
|
|
err = nk.NotificationsSend(ctx, []*runtime.NotificationSend{
|
2026-07-02 16:55:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
Code: 2001,
|
|
|
|
|
|
Content: map[string]interface{}{"type": "tribulation_helper_invite", "inviter_id": req.CharacterID},
|
|
|
|
|
|
Persistent: true,
|
|
|
|
|
|
Subject: "渡劫护法邀请",
|
2026-07-03 21:34:51 +08:00
|
|
|
|
UserID: req.HelperID,
|
2026-07-02 16:55:34 +08:00
|
|
|
|
},
|
2026-07-03 21:34:51 +08:00
|
|
|
|
})
|
2026-07-02 16:55:34 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Error("send notification failed: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return okResp(map[string]interface{}{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"helper_name": helperName,
|
|
|
|
|
|
"message": "护法邀请已发送",
|
|
|
|
|
|
}, traceID)
|
|
|
|
|
|
}
|