后端:资料距离、语音图片会员限制、免短信注册、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:
Your Name
2026-09-04 10:00:08 +08:00
co-authored by Claude Opus 5
parent 842990b0e7
commit acd8933dbb
25 changed files with 1478 additions and 45 deletions
+14 -1
View File
@@ -1,6 +1,7 @@
package app
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
@@ -81,6 +82,12 @@ func (a *App) sendSMS(w http.ResponseWriter, r *http.Request) {
reply(w, data)
}
// smsVerificationRequired reports whether sign-up must carry a code. It follows
// the same sms.enabled switch that lets /auth/sms/send issue one at all.
func (a *App) smsVerificationRequired(ctx context.Context) bool {
return a.configBool(ctx, "sms.enabled", true)
}
func (a *App) register(w http.ResponseWriter, r *http.Request) {
var req authRequest
if err := decode(r, &req); err != nil {
@@ -94,7 +101,13 @@ func (a *App) register(w http.ResponseWriter, r *http.Request) {
if !a.rateLimit(w, r, "register_ip", clientIP(r), 20, 10*time.Minute) || !a.rateLimit(w, r, "register_phone", strings.TrimSpace(req.Phone), 10, 10*time.Minute) {
return
}
if !a.consumeSMSCode(r, req.Phone, "register", req.Code) {
// With the SMS service switched off in the admin console nobody can obtain a
// code, so demanding one would close registration altogether. The switch is
// the operator saying they accept sign-ups without phone verification; the
// per-IP and per-phone limits above are what still holds the door.
// Password reset is deliberately not relaxed the same way: no code there
// means anyone could take over an account by knowing its number.
if a.smsVerificationRequired(r.Context()) && !a.consumeSMSCode(r, req.Phone, "register", req.Code) {
fail(w, http.StatusBadRequest, 20001, "验证码错误或已过期")
return
}