support feedback rewards for unbound users
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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,
|
||||
*,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user