后端:资料距离、语音图片会员限制、免短信注册、AI 托管回复修复
- 单用户资料接口补上距离:此前只有推荐/附近列表会算距离,资料页和 聊天头部因此无内容可显示。沿用同一套 haversine 与隐私开关。 - 语音/图片消息可限定会员发送,两个开关在管理端「运营配置」中修改 (迁移 032)。校验放在 persistMessageContext,HTTP 与 WebSocket 两条发送路径都覆盖;文本消息永不受限。 - 短信服务关闭时注册不再要求验证码:关掉之后没人能拿到验证码,继续 要求就等于关闭注册通道。重置密码不做同样放宽,那里缺验证码等于 凭手机号夺号。app/config 增加 smsVerification 供客户端决定表单形态。 - 修复 AI 托管账号之间不回复:原规则按「发送方是否托管账号」拦截, 把真人操作测试号的正常对话也挡了。改为标记 worker 自己写入的回复, 只对 AI 生成的消息跳过入队。 - ai.default_model_id 同时接受模型 ID 与名称,填名称时不再被 MySQL 静默转成 0 而使配置失效。 - 聊天媒体留存管理与清理任务(迁移 033,两台线上均已应用)。 新增集成测试均针对真实 MySQL:会员限制、免短信注册、AI 入队规则、 默认模型解析、资料距离。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
842990b0e7
commit
acd8933dbb
@@ -120,3 +120,44 @@ func (a *App) reserveDailyActiveChat(ctx context.Context, tx *sql.Tx, conversati
|
||||
_, err = tx.ExecContext(ctx, `UPDATE im_daily_active_chat_usage SET used_count=used_count+1 WHERE user_id=? AND usage_date=CURRENT_DATE()`, senderID)
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
messageTypeImage = 2
|
||||
messageTypeVoice = 3
|
||||
)
|
||||
|
||||
// messageMembershipGateError names the message kind the reader was blocked on,
|
||||
// so the client can offer the right upgrade prompt instead of a generic error.
|
||||
type messageMembershipGateError struct{ Kind string }
|
||||
|
||||
func (e *messageMembershipGateError) Error() string {
|
||||
return fmt.Sprintf("发送%s消息需要开通会员", e.Kind)
|
||||
}
|
||||
|
||||
// hasActiveMembership uses the same subscription row that /membership/status
|
||||
// reports as "active", so the app never offers what the server will refuse.
|
||||
// Every grant path — order, admin, gateway callback — writes that row.
|
||||
func (a *App) hasActiveMembership(ctx context.Context, userID int64) bool {
|
||||
var one int
|
||||
return a.db.QueryRowContext(ctx, `SELECT 1 FROM subscriptions WHERE user_id=? AND status=1 AND started_at<=NOW(3) AND expires_at>NOW(3) LIMIT 1`, userID).Scan(&one) == nil
|
||||
}
|
||||
|
||||
// messageMembershipGate reports whether one message type is members-only and
|
||||
// what to call it. Both switches are edited from the admin console.
|
||||
func (a *App) messageMembershipGate(ctx context.Context, messageType int) (string, bool) {
|
||||
switch messageType {
|
||||
case messageTypeImage:
|
||||
return "图片", a.configBool(ctx, "membership.image_message_requires_vip", false)
|
||||
case messageTypeVoice:
|
||||
return "语音", a.configBool(ctx, "membership.voice_message_requires_vip", false)
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (a *App) ensureMessageMembership(ctx context.Context, senderID int64, messageType int) error {
|
||||
kind, gated := a.messageMembershipGate(ctx, messageType)
|
||||
if !gated || a.hasActiveMembership(ctx, senderID) {
|
||||
return nil
|
||||
}
|
||||
return &messageMembershipGateError{Kind: kind}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user