support feedback rewards for unbound users

This commit is contained in:
2569718930@qq.com
2026-06-12 23:17:34 +08:00
parent 568d22ff1d
commit 33fbf17c18
5 changed files with 114 additions and 9 deletions
@@ -56,4 +56,10 @@ export function runTests() {
feedbackPageSource.includes("reward_points"), feedbackPageSource.includes("reward_points"),
"ops feedback page must show existing feedback reward details", "ops feedback page must show existing feedback reward details",
); );
assert(
feedbackPageSource.includes("actionError") &&
feedbackPageSource.includes("奖励发放失败:") &&
feedbackPageSource.includes("加载失败:"),
"ops feedback page must distinguish reward action failures from list loading failures",
);
} }
@@ -185,7 +185,8 @@ function feedbackFreshnessBadges(context?: Record<string, unknown>) {
export function FeedbackPageClient() { export function FeedbackPageClient() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [loadError, setLoadError] = useState("");
const [actionError, setActionError] = useState("");
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const [payload, setPayload] = useState<UserFeedbackPayload | null>(null); const [payload, setPayload] = useState<UserFeedbackPayload | null>(null);
const [updatingId, setUpdatingId] = useState<number | null>(null); const [updatingId, setUpdatingId] = useState<number | null>(null);
@@ -194,12 +195,12 @@ export function FeedbackPageClient() {
const load = async () => { const load = async () => {
setLoading(true); setLoading(true);
setError(""); setLoadError("");
try { try {
const data = (await opsApi.feedback(120, filter)) as UserFeedbackPayload; const data = (await opsApi.feedback(120, filter)) as UserFeedbackPayload;
setPayload(data); setPayload(data);
} catch (err) { } catch (err) {
setError(String(err).slice(0, 220)); setLoadError(String(err).slice(0, 220));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -246,15 +247,15 @@ export function FeedbackPageClient() {
const selectedPoints = rewardPointsById[row.id] || String(REWARD_POINT_OPTIONS[1].value); const selectedPoints = rewardPointsById[row.id] || String(REWARD_POINT_OPTIONS[1].value);
const points = Number.parseInt(selectedPoints, 10); const points = Number.parseInt(selectedPoints, 10);
if (!row.user_email) { if (!row.user_email) {
setError("这条反馈没有绑定用户邮箱,不能从反馈页直接发放积分。"); setActionError("这条反馈没有绑定用户邮箱,不能从反馈页直接发放积分。");
return; return;
} }
if (!Number.isFinite(points) || points <= 0) { if (!Number.isFinite(points) || points <= 0) {
setError("请输入有效的奖励积分。"); setActionError("请输入有效的奖励积分。");
return; return;
} }
setRewardingId(row.id); setRewardingId(row.id);
setError(""); setActionError("");
try { try {
await opsApi.grantFeedbackReward(row.id, points); await opsApi.grantFeedbackReward(row.id, points);
setRewardPointsById((prev) => { setRewardPointsById((prev) => {
@@ -264,7 +265,7 @@ export function FeedbackPageClient() {
}); });
await load(); await load();
} catch (err) { } catch (err) {
setError(String(err).slice(0, 220)); setActionError(String(err).slice(0, 220));
} finally { } finally {
setRewardingId(null); setRewardingId(null);
} }
@@ -283,9 +284,15 @@ export function FeedbackPageClient() {
</Button> </Button>
</div> </div>
{error && ( {loadError && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{error} {loadError}
</div>
)}
{actionError && (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
{actionError}
</div> </div>
)} )}
+7
View File
@@ -1101,6 +1101,13 @@ class SupabaseEntitlementService:
"points_after": after, "points_after": after,
} }
def grant_points_to_user(
self,
user_id: str,
points: int,
) -> Dict[str, object]:
return self._grant_referral_points(user_id, points)
def _record_referral_resolution_event( def _record_referral_resolution_event(
self, self,
*, *,
+58
View File
@@ -170,6 +170,64 @@ def test_ops_feedback_reward_service_returns_operator(monkeypatch):
assert payload["feedback"]["reward_reason"] == "" assert payload["feedback"]["reward_reason"] == ""
def test_ops_feedback_reward_falls_back_to_supabase_user_without_telegram_binding(monkeypatch):
updated_feedback = {
"id": 19,
"user_id": "supabase-user-19",
"user_email": "927794117@qq.com",
"reward_points": 500,
"reward_reason": "",
"reward_status": "granted",
}
class FakeDB:
def grant_feedback_reward(self, feedback_id, *, points, reason=""):
return {
"ok": False,
"reason": "user_not_found",
"feedback": {
"id": feedback_id,
"user_id": "supabase-user-19",
"user_email": "927794117@qq.com",
},
}
def update_user_feedback_reward(self, feedback_id, *, points, reason="", status="granted"):
assert feedback_id == 19
assert points == 500
assert reason == ""
assert status == "granted"
return updated_feedback
class FakeEntitlement:
def grant_points_to_user(self, user_id, points):
assert user_id == "supabase-user-19"
assert points == 500
return {
"ok": True,
"source": "supabase_metadata",
"points_before": 100,
"points_added": 500,
"points_after": 600,
}
monkeypatch.setattr(ops_api, "_require_ops", lambda request: {"email": "ops@example.com"})
monkeypatch.setattr(ops_api, "DBManager", lambda: FakeDB())
monkeypatch.setattr(ops_api.legacy_routes, "SUPABASE_ENTITLEMENT", FakeEntitlement())
payload = ops_api.grant_ops_feedback_reward(
object(),
feedback_id=19,
points=500,
)
assert payload["ok"] is True
assert payload["source"] == "supabase_metadata"
assert payload["points_after"] == 600
assert payload["feedback"] == updated_feedback
assert payload["operator_email"] == "ops@example.com"
def test_user_feedback_identity_filter_returns_only_matching_user(tmp_path): def test_user_feedback_identity_filter_returns_only_matching_user(tmp_path):
db = DBManager(str(tmp_path / "polyweather-feedback-identity.db")) db = DBManager(str(tmp_path / "polyweather-feedback-identity.db"))
mine_by_user_id = db.append_user_feedback( mine_by_user_id = db.append_user_feedback(
+27
View File
@@ -203,6 +203,33 @@ def grant_ops_feedback_reward(
points=points, points=points,
reason=reason, reason=reason,
) )
if not result.get("ok") and str(result.get("reason") or "") == "user_not_found":
feedback = result.get("feedback") if isinstance(result.get("feedback"), dict) else {}
reward_status = str(feedback.get("reward_status") or "").strip().lower()
reward_points = int(feedback.get("reward_points") or 0)
supabase_user_id = str(feedback.get("user_id") or "").strip().lower()
if supabase_user_id and not (reward_status == "granted" and reward_points > 0):
try:
fallback = legacy_routes.SUPABASE_ENTITLEMENT.grant_points_to_user(
supabase_user_id,
points,
)
except Exception as exc:
fallback = {"ok": False, "reason": f"supabase_points_grant_failed:{exc}"}
if fallback.get("ok"):
updated_feedback = db.update_user_feedback_reward(
feedback_id,
points=points,
reason=reason,
status="granted",
)
result = {
**fallback,
"ok": True,
"feedback_id": int(feedback_id),
"supabase_user_id": supabase_user_id,
"feedback": updated_feedback,
}
result["operator_email"] = admin.get("email") result["operator_email"] = admin.get("email")
if not result.get("ok"): if not result.get("ok"):
reason_code = str(result.get("reason") or "feedback_reward_failed") reason_code = str(result.get("reason") or "feedback_reward_failed")