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"),
"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() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [loadError, setLoadError] = useState("");
const [actionError, setActionError] = useState("");
const [filter, setFilter] = useState("");
const [payload, setPayload] = useState<UserFeedbackPayload | null>(null);
const [updatingId, setUpdatingId] = useState<number | null>(null);
@@ -194,12 +195,12 @@ export function FeedbackPageClient() {
const load = async () => {
setLoading(true);
setError("");
setLoadError("");
try {
const data = (await opsApi.feedback(120, filter)) as UserFeedbackPayload;
setPayload(data);
} catch (err) {
setError(String(err).slice(0, 220));
setLoadError(String(err).slice(0, 220));
} finally {
setLoading(false);
}
@@ -246,15 +247,15 @@ export function FeedbackPageClient() {
const selectedPoints = rewardPointsById[row.id] || String(REWARD_POINT_OPTIONS[1].value);
const points = Number.parseInt(selectedPoints, 10);
if (!row.user_email) {
setError("这条反馈没有绑定用户邮箱,不能从反馈页直接发放积分。");
setActionError("这条反馈没有绑定用户邮箱,不能从反馈页直接发放积分。");
return;
}
if (!Number.isFinite(points) || points <= 0) {
setError("请输入有效的奖励积分。");
setActionError("请输入有效的奖励积分。");
return;
}
setRewardingId(row.id);
setError("");
setActionError("");
try {
await opsApi.grantFeedbackReward(row.id, points);
setRewardPointsById((prev) => {
@@ -264,7 +265,7 @@ export function FeedbackPageClient() {
});
await load();
} catch (err) {
setError(String(err).slice(0, 220));
setActionError(String(err).slice(0, 220));
} finally {
setRewardingId(null);
}
@@ -283,9 +284,15 @@ export function FeedbackPageClient() {
</Button>
</div>
{error && (
{loadError && (
<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>
)}
+7
View File
@@ -1101,6 +1101,13 @@ class SupabaseEntitlementService:
"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(
self,
*,
+58
View File
@@ -170,6 +170,64 @@ def test_ops_feedback_reward_service_returns_operator(monkeypatch):
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):
db = DBManager(str(tmp_path / "polyweather-feedback-identity.db"))
mine_by_user_id = db.append_user_feedback(
+27
View File
@@ -203,6 +203,33 @@ def grant_ops_feedback_reward(
points=points,
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")
if not result.get("ok"):
reason_code = str(result.get("reason") or "feedback_reward_failed")