Add backend feedback submission and image preview
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -116,7 +116,7 @@ use crate::{
|
||||
claim_profile_task_reward, create_profile_recharge_order, get_profile_analytics_metric,
|
||||
get_profile_dashboard, get_profile_play_stats, get_profile_recharge_center,
|
||||
get_profile_referral_invite_center, get_profile_task_center, get_profile_wallet_ledger,
|
||||
redeem_profile_referral_invite_code, redeem_profile_reward_code,
|
||||
redeem_profile_referral_invite_code, redeem_profile_reward_code, submit_profile_feedback,
|
||||
},
|
||||
runtime_save::{
|
||||
delete_runtime_snapshot, get_runtime_snapshot, list_profile_save_archives,
|
||||
@@ -144,6 +144,7 @@ use crate::{
|
||||
};
|
||||
|
||||
const PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES: usize = 12 * 1024 * 1024;
|
||||
const PROFILE_FEEDBACK_BODY_LIMIT_BYTES: usize = 6 * 1024 * 1024;
|
||||
|
||||
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
|
||||
pub fn build_router(state: AppState) -> Router {
|
||||
@@ -1227,6 +1228,16 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/profile/feedback",
|
||||
post(submit_profile_feedback)
|
||||
// 中文注释:反馈首版允许最多四张 1MB Data URL 图片,只给该接口放宽 body limit。
|
||||
.layer(DefaultBodyLimit::max(PROFILE_FEEDBACK_BODY_LIMIT_BYTES))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/profile/referrals/invite-center",
|
||||
get(get_profile_referral_invite_center).route_layer(middleware::from_fn_with_state(
|
||||
|
||||
@@ -100,9 +100,9 @@ fn run_api_server_with_runtime() -> Result<(), std::io::Error> {
|
||||
|
||||
async fn run_api_server() -> Result<(), std::io::Error> {
|
||||
// 运行本地开发与联调时,优先从仓库根目录加载本地变量,避免手工逐项导出 OSS / APIMart 配置。
|
||||
let _ = dotenvy::from_filename(".env");
|
||||
let _ = dotenvy::from_filename(".env.local");
|
||||
let _ = dotenvy::from_filename(".env.secrets.local");
|
||||
let _ = dotenvy::from_filename(".env.local");
|
||||
let _ = dotenvy::from_filename(".env");
|
||||
|
||||
// 统一先从配置对象读取监听地址,避免后续把环境变量读取散落到入口和路由层。
|
||||
let config = AppConfig::from_env();
|
||||
|
||||
@@ -5,7 +5,9 @@ use axum::{
|
||||
response::Response,
|
||||
};
|
||||
use module_runtime::{
|
||||
AnalyticsGranularity, PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, RuntimeProfileInviteCodeRecord,
|
||||
AnalyticsGranularity, PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK,
|
||||
RuntimeProfileFeedbackEvidenceRecord, RuntimeProfileFeedbackEvidenceSnapshot,
|
||||
RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeRecord,
|
||||
RuntimeProfileMembershipBenefitRecord, RuntimeProfileRechargeCenterRecord,
|
||||
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeProductRecord,
|
||||
RuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord,
|
||||
@@ -23,8 +25,8 @@ use shared_contracts::runtime::{
|
||||
AdminUpsertProfileRedeemCodeRequest, AdminUpsertProfileTaskConfigRequest,
|
||||
AnalyticsBucketMetricResponse, AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
|
||||
CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse,
|
||||
PROFILE_TASK_CYCLE_DAILY, PROFILE_TASK_STATUS_CLAIMABLE, PROFILE_TASK_STATUS_CLAIMED,
|
||||
PROFILE_TASK_STATUS_DISABLED, PROFILE_TASK_STATUS_INCOMPLETE,
|
||||
PROFILE_FEEDBACK_STATUS_OPEN, PROFILE_TASK_CYCLE_DAILY, PROFILE_TASK_STATUS_CLAIMABLE,
|
||||
PROFILE_TASK_STATUS_CLAIMED, PROFILE_TASK_STATUS_DISABLED, PROFILE_TASK_STATUS_INCOMPLETE,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD,
|
||||
@@ -35,6 +37,7 @@ use shared_contracts::runtime::{
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC, ProfileDashboardSummaryResponse,
|
||||
ProfileFeedbackEvidenceItemResponse, ProfileFeedbackSubmissionResponse,
|
||||
ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse,
|
||||
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
|
||||
ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse, ProfileRechargeOrderResponse,
|
||||
@@ -44,8 +47,9 @@ use shared_contracts::runtime::{
|
||||
ProfileTaskConfigAdminListResponse, ProfileTaskConfigAdminResponse, ProfileTaskItemResponse,
|
||||
ProfileWalletLedgerEntryResponse, ProfileWalletLedgerResponse,
|
||||
RedeemProfileReferralInviteCodeRequest, RedeemProfileReferralInviteCodeResponse,
|
||||
RedeemProfileRewardCodeRequest, RedeemProfileRewardCodeResponse, TRACKING_SCOPE_KIND_MODULE,
|
||||
TRACKING_SCOPE_KIND_SITE, TRACKING_SCOPE_KIND_USER, TRACKING_SCOPE_KIND_WORK,
|
||||
RedeemProfileRewardCodeRequest, RedeemProfileRewardCodeResponse, SubmitProfileFeedbackRequest,
|
||||
SubmitProfileFeedbackResponse, TRACKING_SCOPE_KIND_MODULE, TRACKING_SCOPE_KIND_SITE,
|
||||
TRACKING_SCOPE_KIND_USER, TRACKING_SCOPE_KIND_WORK,
|
||||
};
|
||||
use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339};
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
@@ -208,6 +212,51 @@ pub async fn create_profile_recharge_order(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn submit_profile_feedback(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<SubmitProfileFeedbackRequest>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let user_id = authenticated.claims().user_id().to_string();
|
||||
let created_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
|
||||
let evidence_items = payload
|
||||
.evidence_items
|
||||
.into_iter()
|
||||
.map(|item| RuntimeProfileFeedbackEvidenceSnapshot {
|
||||
evidence_id: String::new(),
|
||||
file_name: item.file_name,
|
||||
content_type: item.content_type,
|
||||
size_bytes: item.size_bytes,
|
||||
data_url: item.data_url,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
.submit_profile_feedback(
|
||||
user_id,
|
||||
payload.description,
|
||||
payload.contact_phone,
|
||||
evidence_items,
|
||||
created_at_micros as i64,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
map_runtime_profile_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
SubmitProfileFeedbackResponse {
|
||||
feedback: build_profile_feedback_submission_response(record),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_profile_referral_invite_center(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -782,6 +831,36 @@ fn build_profile_recharge_order_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_feedback_submission_response(
|
||||
record: RuntimeProfileFeedbackSubmissionRecord,
|
||||
) -> ProfileFeedbackSubmissionResponse {
|
||||
ProfileFeedbackSubmissionResponse {
|
||||
feedback_id: record.feedback_id,
|
||||
status: match record.status {
|
||||
module_runtime::RuntimeProfileFeedbackStatus::Open => {
|
||||
PROFILE_FEEDBACK_STATUS_OPEN.to_string()
|
||||
}
|
||||
},
|
||||
created_at: record.created_at,
|
||||
evidence_items: record
|
||||
.evidence_items
|
||||
.into_iter()
|
||||
.map(build_profile_feedback_evidence_response)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_feedback_evidence_response(
|
||||
record: RuntimeProfileFeedbackEvidenceRecord,
|
||||
) -> ProfileFeedbackEvidenceItemResponse {
|
||||
ProfileFeedbackEvidenceItemResponse {
|
||||
evidence_id: record.evidence_id,
|
||||
file_name: record.file_name,
|
||||
content_type: record.content_type,
|
||||
size_bytes: record.size_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_referral_invite_center_response(
|
||||
record: RuntimeReferralInviteCenterRecord,
|
||||
) -> ProfileReferralInviteCenterResponse {
|
||||
@@ -1245,6 +1324,27 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_feedback_requires_authentication() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/profile/feedback")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"description":"反馈页面上传图片后没有显示预览"}"#,
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_referral_invite_center_requires_authentication() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
@@ -1345,6 +1445,7 @@ mod tests {
|
||||
"/api/runtime/profile/wallet-ledger",
|
||||
"/api/runtime/profile/recharge-center",
|
||||
"/api/runtime/profile/recharge/orders",
|
||||
"/api/runtime/profile/feedback",
|
||||
"/api/runtime/profile/referrals/invite-center",
|
||||
"/api/runtime/profile/referrals/redeem-code",
|
||||
"/api/runtime/profile/redeem-codes/redeem",
|
||||
|
||||
@@ -164,6 +164,36 @@ pub fn build_runtime_profile_recharge_order_record(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_feedback_submission_record(
|
||||
snapshot: RuntimeProfileFeedbackSubmissionSnapshot,
|
||||
) -> Result<RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileFieldError> {
|
||||
let evidence_items = serde_json::from_str::<Vec<RuntimeProfileFeedbackEvidenceSnapshot>>(
|
||||
&snapshot.evidence_json,
|
||||
)
|
||||
.map_err(|_| RuntimeProfileFieldError::InvalidFeedbackEvidenceDataUrl)?
|
||||
.into_iter()
|
||||
.map(|item| RuntimeProfileFeedbackEvidenceRecord {
|
||||
evidence_id: item.evidence_id,
|
||||
file_name: item.file_name,
|
||||
content_type: item.content_type,
|
||||
size_bytes: item.size_bytes,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(RuntimeProfileFeedbackSubmissionRecord {
|
||||
feedback_id: snapshot.feedback_id,
|
||||
user_id: snapshot.user_id,
|
||||
description: snapshot.description,
|
||||
contact_phone: snapshot.contact_phone,
|
||||
evidence_items,
|
||||
status: snapshot.status,
|
||||
created_at: format_utc_micros(snapshot.created_at_micros),
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at: format_utc_micros(snapshot.updated_at_micros),
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_referral_invite_center_record(
|
||||
snapshot: RuntimeReferralInviteCenterSnapshot,
|
||||
) -> RuntimeReferralInviteCenterRecord {
|
||||
|
||||
@@ -262,6 +262,83 @@ pub fn build_runtime_profile_recharge_order_create_input(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_feedback_submission_input(
|
||||
user_id: String,
|
||||
description: String,
|
||||
contact_phone: Option<String>,
|
||||
evidence_items: Vec<RuntimeProfileFeedbackEvidenceSnapshot>,
|
||||
created_at_micros: i64,
|
||||
) -> Result<RuntimeProfileFeedbackSubmissionInput, RuntimeProfileFieldError> {
|
||||
let user_id = normalize_runtime_profile_user_id(user_id)?;
|
||||
let description = normalize_required_string(description)
|
||||
.ok_or(RuntimeProfileFieldError::MissingFeedbackDescription)?;
|
||||
let description_chars = description.chars().count();
|
||||
if description_chars < PROFILE_FEEDBACK_DESCRIPTION_MIN_CHARS {
|
||||
return Err(RuntimeProfileFieldError::FeedbackDescriptionTooShort);
|
||||
}
|
||||
if description_chars > PROFILE_FEEDBACK_DESCRIPTION_MAX_CHARS {
|
||||
return Err(RuntimeProfileFieldError::FeedbackDescriptionTooLong);
|
||||
}
|
||||
|
||||
let contact_phone = normalize_optional_string(contact_phone);
|
||||
if contact_phone
|
||||
.as_deref()
|
||||
.map(|value| value.chars().count() > PROFILE_FEEDBACK_CONTACT_PHONE_MAX_CHARS)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(RuntimeProfileFieldError::FeedbackContactPhoneTooLong);
|
||||
}
|
||||
if evidence_items.len() > PROFILE_FEEDBACK_EVIDENCE_MAX_COUNT {
|
||||
return Err(RuntimeProfileFieldError::TooManyFeedbackEvidenceItems);
|
||||
}
|
||||
|
||||
let feedback_id = build_runtime_profile_feedback_submission_id(&user_id, created_at_micros);
|
||||
let mut total_size_bytes = 0u64;
|
||||
let mut normalized_evidence_items = Vec::with_capacity(evidence_items.len());
|
||||
for (index, item) in evidence_items.into_iter().enumerate() {
|
||||
let content_type = normalize_required_string(item.content_type)
|
||||
.map(|value| value.to_ascii_lowercase())
|
||||
.ok_or(RuntimeProfileFieldError::InvalidFeedbackEvidenceContentType)?;
|
||||
if !is_profile_feedback_image_content_type(&content_type) {
|
||||
return Err(RuntimeProfileFieldError::InvalidFeedbackEvidenceContentType);
|
||||
}
|
||||
if item.size_bytes == 0 || item.size_bytes > PROFILE_FEEDBACK_EVIDENCE_MAX_BYTES {
|
||||
return Err(RuntimeProfileFieldError::FeedbackEvidenceTooLarge);
|
||||
}
|
||||
|
||||
total_size_bytes = total_size_bytes
|
||||
.checked_add(item.size_bytes)
|
||||
.ok_or(RuntimeProfileFieldError::FeedbackEvidenceTotalTooLarge)?;
|
||||
if total_size_bytes > PROFILE_FEEDBACK_EVIDENCE_TOTAL_MAX_BYTES {
|
||||
return Err(RuntimeProfileFieldError::FeedbackEvidenceTotalTooLarge);
|
||||
}
|
||||
|
||||
let data_url = normalize_required_string(item.data_url)
|
||||
.ok_or(RuntimeProfileFieldError::InvalidFeedbackEvidenceDataUrl)?;
|
||||
if !has_profile_feedback_data_url_prefix(&data_url, &content_type) {
|
||||
return Err(RuntimeProfileFieldError::InvalidFeedbackEvidenceDataUrl);
|
||||
}
|
||||
let file_name = normalize_optional_string(Some(item.file_name))
|
||||
.unwrap_or_else(|| format!("feedback-image-{}", index + 1));
|
||||
|
||||
normalized_evidence_items.push(RuntimeProfileFeedbackEvidenceSnapshot {
|
||||
evidence_id: build_runtime_profile_feedback_evidence_id(&feedback_id, index),
|
||||
file_name,
|
||||
content_type,
|
||||
size_bytes: item.size_bytes,
|
||||
data_url,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(RuntimeProfileFeedbackSubmissionInput {
|
||||
user_id,
|
||||
description,
|
||||
contact_phone,
|
||||
evidence_items: normalized_evidence_items,
|
||||
created_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_referral_invite_center_get_input(
|
||||
user_id: String,
|
||||
) -> Result<RuntimeReferralInviteCenterGetInput, RuntimeProfileFieldError> {
|
||||
@@ -595,6 +672,17 @@ pub fn build_runtime_browse_history_id(
|
||||
format!("{user_id}:{owner_user_id}:{profile_id}")
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_feedback_submission_id(
|
||||
user_id: &str,
|
||||
created_at_micros: i64,
|
||||
) -> String {
|
||||
format!("feedback:{}:{}", user_id.trim(), created_at_micros)
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_feedback_evidence_id(feedback_id: &str, index: usize) -> String {
|
||||
format!("{}:evidence:{:02}", feedback_id.trim(), index + 1)
|
||||
}
|
||||
|
||||
fn parse_utc_rfc3339_to_micros(value: &str) -> Option<i64> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -630,6 +718,19 @@ fn normalize_current_story_json(
|
||||
.map_err(|_| RuntimeProfileFieldError::InvalidCurrentStoryJson)
|
||||
}
|
||||
|
||||
fn is_profile_feedback_image_content_type(value: &str) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
"image/png" | "image/jpeg" | "image/jpg" | "image/webp" | "image/gif"
|
||||
)
|
||||
}
|
||||
|
||||
fn has_profile_feedback_data_url_prefix(data_url: &str, content_type: &str) -> bool {
|
||||
data_url
|
||||
.to_ascii_lowercase()
|
||||
.starts_with(&format!("data:{content_type};base64,"))
|
||||
}
|
||||
|
||||
pub fn normalize_invite_code(value: String) -> Option<String> {
|
||||
let normalized = value
|
||||
.trim()
|
||||
|
||||
@@ -33,6 +33,12 @@ pub const PROFILE_TASK_DEFAULT_THRESHOLD: u32 = 1;
|
||||
pub const SAVE_SNAPSHOT_VERSION: u32 = 2;
|
||||
pub const DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT: &str = "继续推进上一次保存的故事。";
|
||||
pub const PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK: &str = "mock";
|
||||
pub const PROFILE_FEEDBACK_DESCRIPTION_MIN_CHARS: usize = 10;
|
||||
pub const PROFILE_FEEDBACK_DESCRIPTION_MAX_CHARS: usize = 200;
|
||||
pub const PROFILE_FEEDBACK_CONTACT_PHONE_MAX_CHARS: usize = 40;
|
||||
pub const PROFILE_FEEDBACK_EVIDENCE_MAX_COUNT: usize = 4;
|
||||
pub const PROFILE_FEEDBACK_EVIDENCE_MAX_BYTES: u64 = 1_048_576;
|
||||
pub const PROFILE_FEEDBACK_EVIDENCE_TOTAL_MAX_BYTES: u64 = 4_194_304;
|
||||
|
||||
/// 分析日期维表的纯领域快照。
|
||||
///
|
||||
@@ -440,6 +446,83 @@ pub struct RuntimeProfileDashboardGetInput {
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeProfileFeedbackStatus {
|
||||
Open,
|
||||
}
|
||||
|
||||
impl RuntimeProfileFeedbackStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Open => "open",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileFeedbackEvidenceSnapshot {
|
||||
pub evidence_id: String,
|
||||
pub file_name: String,
|
||||
pub content_type: String,
|
||||
pub size_bytes: u64,
|
||||
pub data_url: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileFeedbackSubmissionInput {
|
||||
pub user_id: String,
|
||||
pub description: String,
|
||||
pub contact_phone: Option<String>,
|
||||
pub evidence_items: Vec<RuntimeProfileFeedbackEvidenceSnapshot>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileFeedbackSubmissionSnapshot {
|
||||
pub feedback_id: String,
|
||||
pub user_id: String,
|
||||
pub description: String,
|
||||
pub contact_phone: Option<String>,
|
||||
pub evidence_json: String,
|
||||
pub status: RuntimeProfileFeedbackStatus,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileFeedbackSubmissionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<RuntimeProfileFeedbackSubmissionSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeProfileFeedbackEvidenceRecord {
|
||||
pub evidence_id: String,
|
||||
pub file_name: String,
|
||||
pub content_type: String,
|
||||
pub size_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeProfileFeedbackSubmissionRecord {
|
||||
pub feedback_id: String,
|
||||
pub user_id: String,
|
||||
pub description: String,
|
||||
pub contact_phone: Option<String>,
|
||||
pub evidence_items: Vec<RuntimeProfileFeedbackEvidenceRecord>,
|
||||
pub status: RuntimeProfileFeedbackStatus,
|
||||
pub created_at: String,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeTrackingScopeKind {
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
//!
|
||||
//! 错误保持运行时业务语义,例如快照版本非法、兑换码不可用或钱包余额不足。
|
||||
|
||||
use crate::MAX_BROWSE_HISTORY_BATCH_SIZE;
|
||||
use crate::{
|
||||
MAX_BROWSE_HISTORY_BATCH_SIZE, PROFILE_FEEDBACK_CONTACT_PHONE_MAX_CHARS,
|
||||
PROFILE_FEEDBACK_DESCRIPTION_MAX_CHARS, PROFILE_FEEDBACK_DESCRIPTION_MIN_CHARS,
|
||||
PROFILE_FEEDBACK_EVIDENCE_MAX_BYTES, PROFILE_FEEDBACK_EVIDENCE_MAX_COUNT,
|
||||
PROFILE_FEEDBACK_EVIDENCE_TOTAL_MAX_BYTES,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RuntimeSettingsFieldError {
|
||||
@@ -80,6 +85,15 @@ pub enum RuntimeProfileFieldError {
|
||||
},
|
||||
NonPersistentRuntimeSnapshot,
|
||||
InvalidAnalyticsCalendarDate,
|
||||
MissingFeedbackDescription,
|
||||
FeedbackDescriptionTooShort,
|
||||
FeedbackDescriptionTooLong,
|
||||
FeedbackContactPhoneTooLong,
|
||||
TooManyFeedbackEvidenceItems,
|
||||
InvalidFeedbackEvidenceContentType,
|
||||
InvalidFeedbackEvidenceDataUrl,
|
||||
FeedbackEvidenceTooLarge,
|
||||
FeedbackEvidenceTotalTooLarge,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RuntimeProfileFieldError {
|
||||
@@ -140,6 +154,37 @@ impl std::fmt::Display for RuntimeProfileFieldError {
|
||||
Self::InvalidAnalyticsCalendarDate => {
|
||||
f.write_str("analytics_date_dimension.calendar_date 必须是合法 YYYY-MM-DD 日期")
|
||||
}
|
||||
Self::MissingFeedbackDescription => f.write_str("反馈问题描述不能为空"),
|
||||
Self::FeedbackDescriptionTooShort => write!(
|
||||
f,
|
||||
"请填写{}个字以上的问题描述",
|
||||
PROFILE_FEEDBACK_DESCRIPTION_MIN_CHARS
|
||||
),
|
||||
Self::FeedbackDescriptionTooLong => write!(
|
||||
f,
|
||||
"问题描述不能超过 {} 字",
|
||||
PROFILE_FEEDBACK_DESCRIPTION_MAX_CHARS
|
||||
),
|
||||
Self::FeedbackContactPhoneTooLong => write!(
|
||||
f,
|
||||
"联系电话不能超过 {} 字",
|
||||
PROFILE_FEEDBACK_CONTACT_PHONE_MAX_CHARS
|
||||
),
|
||||
Self::TooManyFeedbackEvidenceItems => {
|
||||
write!(f, "最多上传{}张凭证", PROFILE_FEEDBACK_EVIDENCE_MAX_COUNT)
|
||||
}
|
||||
Self::InvalidFeedbackEvidenceContentType => f.write_str("反馈凭证只支持图片类型"),
|
||||
Self::InvalidFeedbackEvidenceDataUrl => f.write_str("反馈凭证图片数据无效"),
|
||||
Self::FeedbackEvidenceTooLarge => write!(
|
||||
f,
|
||||
"单张反馈凭证不能超过 {} bytes",
|
||||
PROFILE_FEEDBACK_EVIDENCE_MAX_BYTES
|
||||
),
|
||||
Self::FeedbackEvidenceTotalTooLarge => write!(
|
||||
f,
|
||||
"反馈凭证总大小不能超过 {} bytes",
|
||||
PROFILE_FEEDBACK_EVIDENCE_TOTAL_MAX_BYTES
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub const PROFILE_TASK_STATUS_INCOMPLETE: &str = "incomplete";
|
||||
pub const PROFILE_TASK_STATUS_CLAIMABLE: &str = "claimable";
|
||||
pub const PROFILE_TASK_STATUS_CLAIMED: &str = "claimed";
|
||||
pub const PROFILE_TASK_STATUS_DISABLED: &str = "disabled";
|
||||
pub const PROFILE_FEEDBACK_STATUS_OPEN: &str = "open";
|
||||
pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
|
||||
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
|
||||
pub const TRACKING_SCOPE_KIND_MODULE: &str = "module";
|
||||
@@ -254,6 +255,49 @@ pub struct CreateProfileRechargeOrderResponse {
|
||||
pub center: ProfileRechargeCenterResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileFeedbackEvidenceItemRequest {
|
||||
pub file_name: String,
|
||||
pub content_type: String,
|
||||
pub size_bytes: u64,
|
||||
pub data_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubmitProfileFeedbackRequest {
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub contact_phone: Option<String>,
|
||||
#[serde(default)]
|
||||
pub evidence_items: Vec<ProfileFeedbackEvidenceItemRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileFeedbackEvidenceItemResponse {
|
||||
pub evidence_id: String,
|
||||
pub file_name: String,
|
||||
pub content_type: String,
|
||||
pub size_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileFeedbackSubmissionResponse {
|
||||
pub feedback_id: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub evidence_items: Vec<ProfileFeedbackEvidenceItemResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubmitProfileFeedbackResponse {
|
||||
pub feedback: ProfileFeedbackSubmissionResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileReferralInvitedUserResponse {
|
||||
@@ -1263,6 +1307,41 @@ mod tests {
|
||||
assert_eq!(payload.payment_channel, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_feedback_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(SubmitProfileFeedbackResponse {
|
||||
feedback: ProfileFeedbackSubmissionResponse {
|
||||
feedback_id: "feedback:user-1:1".to_string(),
|
||||
status: PROFILE_FEEDBACK_STATUS_OPEN.to_string(),
|
||||
created_at: "2026-05-08T10:00:00Z".to_string(),
|
||||
evidence_items: vec![ProfileFeedbackEvidenceItemResponse {
|
||||
evidence_id: "feedback:user-1:1:evidence:01".to_string(),
|
||||
file_name: "问题截图.png".to_string(),
|
||||
content_type: "image/png".to_string(),
|
||||
size_bytes: 128,
|
||||
}],
|
||||
},
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload["feedback"]["feedbackId"],
|
||||
json!("feedback:user-1:1")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["feedback"]["status"],
|
||||
json!(PROFILE_FEEDBACK_STATUS_OPEN)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["feedback"]["evidenceItems"][0]["contentType"],
|
||||
json!("image/png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["feedback"]["evidenceItems"][0]["sizeBytes"],
|
||||
json!(128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_play_stats_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ProfilePlayStatsResponse {
|
||||
|
||||
@@ -146,8 +146,9 @@ use module_puzzle::{
|
||||
use module_runtime::{
|
||||
AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse, RuntimeBrowseHistoryRecord,
|
||||
RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord,
|
||||
RuntimeProfileInviteCodeRecord, RuntimeProfilePlayStatsRecord,
|
||||
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
||||
RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeRecord,
|
||||
RuntimeProfilePlayStatsRecord, RuntimeProfileRechargeCenterRecord,
|
||||
RuntimeProfileRechargeOrderRecord,
|
||||
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
|
||||
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
||||
RuntimeProfileSaveArchiveRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord,
|
||||
@@ -159,6 +160,8 @@ use module_runtime::{
|
||||
build_runtime_browse_history_clear_input, build_runtime_browse_history_list_input,
|
||||
build_runtime_browse_history_record, build_runtime_browse_history_sync_input,
|
||||
build_runtime_profile_dashboard_get_input, build_runtime_profile_dashboard_record,
|
||||
build_runtime_profile_feedback_submission_input,
|
||||
build_runtime_profile_feedback_submission_record,
|
||||
build_runtime_profile_invite_code_admin_list_input,
|
||||
build_runtime_profile_invite_code_admin_upsert_input, build_runtime_profile_invite_code_record,
|
||||
build_runtime_profile_play_stats_get_input, build_runtime_profile_play_stats_record,
|
||||
|
||||
@@ -161,6 +161,34 @@ impl From<module_runtime::RuntimeProfileRechargeOrderCreateInput>
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileFeedbackSubmissionInput>
|
||||
for RuntimeProfileFeedbackSubmissionInput
|
||||
{
|
||||
fn from(input: module_runtime::RuntimeProfileFeedbackSubmissionInput) -> Self {
|
||||
Self {
|
||||
user_id: input.user_id,
|
||||
description: input.description,
|
||||
contact_phone: input.contact_phone,
|
||||
evidence_items: input.evidence_items.into_iter().map(Into::into).collect(),
|
||||
created_at_micros: input.created_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileFeedbackEvidenceSnapshot>
|
||||
for RuntimeProfileFeedbackEvidenceSnapshot
|
||||
{
|
||||
fn from(input: module_runtime::RuntimeProfileFeedbackEvidenceSnapshot) -> Self {
|
||||
Self {
|
||||
evidence_id: input.evidence_id,
|
||||
file_name: input.file_name,
|
||||
content_type: input.content_type,
|
||||
size_bytes: input.size_bytes,
|
||||
data_url: input.data_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileRewardCodeRedeemInput>
|
||||
for RuntimeProfileRewardCodeRedeemInput
|
||||
{
|
||||
@@ -846,6 +874,23 @@ pub(crate) fn map_runtime_profile_recharge_order_procedure_result(
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_feedback_submission_procedure_result(
|
||||
result: RuntimeProfileFeedbackSubmissionProcedureResult,
|
||||
) -> Result<RuntimeProfileFeedbackSubmissionRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let snapshot = result
|
||||
.record
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("profile feedback 快照"))?;
|
||||
|
||||
build_runtime_profile_feedback_submission_record(
|
||||
map_runtime_profile_feedback_submission_snapshot(snapshot),
|
||||
)
|
||||
.map_err(SpacetimeClientError::validation_failed)
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_referral_invite_center_procedure_result(
|
||||
result: RuntimeReferralInviteCenterProcedureResult,
|
||||
) -> Result<RuntimeReferralInviteCenterRecord, SpacetimeClientError> {
|
||||
@@ -1999,6 +2044,21 @@ pub(crate) fn map_runtime_profile_recharge_order_snapshot(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_feedback_submission_snapshot(
|
||||
snapshot: RuntimeProfileFeedbackSubmissionSnapshot,
|
||||
) -> module_runtime::RuntimeProfileFeedbackSubmissionSnapshot {
|
||||
module_runtime::RuntimeProfileFeedbackSubmissionSnapshot {
|
||||
feedback_id: snapshot.feedback_id,
|
||||
user_id: snapshot.user_id,
|
||||
description: snapshot.description,
|
||||
contact_phone: snapshot.contact_phone,
|
||||
evidence_json: snapshot.evidence_json,
|
||||
status: map_runtime_profile_feedback_status_back(snapshot.status),
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_referral_invite_center_snapshot(
|
||||
snapshot: RuntimeReferralInviteCenterSnapshot,
|
||||
) -> module_runtime::RuntimeReferralInviteCenterSnapshot {
|
||||
@@ -4644,6 +4704,16 @@ pub(crate) fn map_runtime_profile_recharge_order_status_back(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_feedback_status_back(
|
||||
value: crate::module_bindings::RuntimeProfileFeedbackStatus,
|
||||
) -> module_runtime::RuntimeProfileFeedbackStatus {
|
||||
match value {
|
||||
crate::module_bindings::RuntimeProfileFeedbackStatus::Open => {
|
||||
module_runtime::RuntimeProfileFeedbackStatus::Open
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_session_status(value: StorySessionStatus) -> DomainStorySessionStatus {
|
||||
match value {
|
||||
StorySessionStatus::Active => DomainStorySessionStatus::Active,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
// This was generated using spacetimedb cli version 2.1.0 (commit 10a4779b1338eff3708493d87496b51842a7c412).
|
||||
// This was generated using spacetimedb cli version 2.2.0 (commit eb11e2f5c41dce6979715ad407996270d61329f6).
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
@@ -399,6 +399,8 @@ pub mod player_progression_table;
|
||||
pub mod player_progression_type;
|
||||
pub mod profile_dashboard_state_table;
|
||||
pub mod profile_dashboard_state_type;
|
||||
pub mod profile_feedback_submission_table;
|
||||
pub mod profile_feedback_submission_type;
|
||||
pub mod profile_invite_code_table;
|
||||
pub mod profile_invite_code_type;
|
||||
pub mod profile_membership_table;
|
||||
@@ -567,6 +569,11 @@ pub mod runtime_platform_theme_type;
|
||||
pub mod runtime_profile_dashboard_get_input_type;
|
||||
pub mod runtime_profile_dashboard_procedure_result_type;
|
||||
pub mod runtime_profile_dashboard_snapshot_type;
|
||||
pub mod runtime_profile_feedback_evidence_snapshot_type;
|
||||
pub mod runtime_profile_feedback_status_type;
|
||||
pub mod runtime_profile_feedback_submission_input_type;
|
||||
pub mod runtime_profile_feedback_submission_procedure_result_type;
|
||||
pub mod runtime_profile_feedback_submission_snapshot_type;
|
||||
pub mod runtime_profile_invite_code_admin_list_input_type;
|
||||
pub mod runtime_profile_invite_code_admin_list_procedure_result_type;
|
||||
pub mod runtime_profile_invite_code_admin_procedure_result_type;
|
||||
@@ -643,8 +650,8 @@ pub mod runtime_snapshot_row_type;
|
||||
pub mod runtime_snapshot_table;
|
||||
pub mod runtime_snapshot_type;
|
||||
pub mod runtime_snapshot_upsert_input_type;
|
||||
pub mod runtime_tracking_scope_kind_type;
|
||||
pub mod runtime_tracking_event_procedure_result_type;
|
||||
pub mod runtime_tracking_scope_kind_type;
|
||||
pub mod save_puzzle_form_draft_procedure;
|
||||
pub mod save_puzzle_generated_images_procedure;
|
||||
pub mod seed_analytics_date_dimensions_reducer;
|
||||
@@ -703,6 +710,7 @@ pub mod submit_big_fish_input_procedure;
|
||||
pub mod submit_big_fish_message_procedure;
|
||||
pub mod submit_custom_world_agent_message_procedure;
|
||||
pub mod submit_match_3_d_agent_message_procedure;
|
||||
pub mod submit_profile_feedback_and_return_procedure;
|
||||
pub mod submit_puzzle_agent_message_procedure;
|
||||
pub mod submit_puzzle_leaderboard_entry_procedure;
|
||||
pub mod submit_square_hole_agent_message_procedure;
|
||||
@@ -1135,6 +1143,8 @@ pub use player_progression_table::*;
|
||||
pub use player_progression_type::PlayerProgression;
|
||||
pub use profile_dashboard_state_table::*;
|
||||
pub use profile_dashboard_state_type::ProfileDashboardState;
|
||||
pub use profile_feedback_submission_table::*;
|
||||
pub use profile_feedback_submission_type::ProfileFeedbackSubmission;
|
||||
pub use profile_invite_code_table::*;
|
||||
pub use profile_invite_code_type::ProfileInviteCode;
|
||||
pub use profile_membership_table::*;
|
||||
@@ -1303,6 +1313,11 @@ pub use runtime_platform_theme_type::RuntimePlatformTheme;
|
||||
pub use runtime_profile_dashboard_get_input_type::RuntimeProfileDashboardGetInput;
|
||||
pub use runtime_profile_dashboard_procedure_result_type::RuntimeProfileDashboardProcedureResult;
|
||||
pub use runtime_profile_dashboard_snapshot_type::RuntimeProfileDashboardSnapshot;
|
||||
pub use runtime_profile_feedback_evidence_snapshot_type::RuntimeProfileFeedbackEvidenceSnapshot;
|
||||
pub use runtime_profile_feedback_status_type::RuntimeProfileFeedbackStatus;
|
||||
pub use runtime_profile_feedback_submission_input_type::RuntimeProfileFeedbackSubmissionInput;
|
||||
pub use runtime_profile_feedback_submission_procedure_result_type::RuntimeProfileFeedbackSubmissionProcedureResult;
|
||||
pub use runtime_profile_feedback_submission_snapshot_type::RuntimeProfileFeedbackSubmissionSnapshot;
|
||||
pub use runtime_profile_invite_code_admin_list_input_type::RuntimeProfileInviteCodeAdminListInput;
|
||||
pub use runtime_profile_invite_code_admin_list_procedure_result_type::RuntimeProfileInviteCodeAdminListProcedureResult;
|
||||
pub use runtime_profile_invite_code_admin_procedure_result_type::RuntimeProfileInviteCodeAdminProcedureResult;
|
||||
@@ -1379,8 +1394,8 @@ pub use runtime_snapshot_row_type::RuntimeSnapshotRow;
|
||||
pub use runtime_snapshot_table::*;
|
||||
pub use runtime_snapshot_type::RuntimeSnapshot;
|
||||
pub use runtime_snapshot_upsert_input_type::RuntimeSnapshotUpsertInput;
|
||||
pub use runtime_tracking_scope_kind_type::RuntimeTrackingScopeKind;
|
||||
pub use runtime_tracking_event_procedure_result_type::RuntimeTrackingEventProcedureResult;
|
||||
pub use runtime_tracking_scope_kind_type::RuntimeTrackingScopeKind;
|
||||
pub use save_puzzle_form_draft_procedure::save_puzzle_form_draft;
|
||||
pub use save_puzzle_generated_images_procedure::save_puzzle_generated_images;
|
||||
pub use seed_analytics_date_dimensions_reducer::seed_analytics_date_dimensions;
|
||||
@@ -1439,6 +1454,7 @@ pub use submit_big_fish_input_procedure::submit_big_fish_input;
|
||||
pub use submit_big_fish_message_procedure::submit_big_fish_message;
|
||||
pub use submit_custom_world_agent_message_procedure::submit_custom_world_agent_message;
|
||||
pub use submit_match_3_d_agent_message_procedure::submit_match_3_d_agent_message;
|
||||
pub use submit_profile_feedback_and_return_procedure::submit_profile_feedback_and_return;
|
||||
pub use submit_puzzle_agent_message_procedure::submit_puzzle_agent_message;
|
||||
pub use submit_puzzle_leaderboard_entry_procedure::submit_puzzle_leaderboard_entry;
|
||||
pub use submit_square_hole_agent_message_procedure::submit_square_hole_agent_message;
|
||||
@@ -1785,6 +1801,7 @@ pub struct DbUpdate {
|
||||
npc_state: __sdk::TableUpdate<NpcState>,
|
||||
player_progression: __sdk::TableUpdate<PlayerProgression>,
|
||||
profile_dashboard_state: __sdk::TableUpdate<ProfileDashboardState>,
|
||||
profile_feedback_submission: __sdk::TableUpdate<ProfileFeedbackSubmission>,
|
||||
profile_invite_code: __sdk::TableUpdate<ProfileInviteCode>,
|
||||
profile_membership: __sdk::TableUpdate<ProfileMembership>,
|
||||
profile_played_world: __sdk::TableUpdate<ProfilePlayedWorld>,
|
||||
@@ -1936,6 +1953,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
||||
"profile_dashboard_state" => db_update.profile_dashboard_state.append(
|
||||
profile_dashboard_state_table::parse_table_update(table_update)?,
|
||||
),
|
||||
"profile_feedback_submission" => db_update.profile_feedback_submission.append(
|
||||
profile_feedback_submission_table::parse_table_update(table_update)?,
|
||||
),
|
||||
"profile_invite_code" => db_update
|
||||
.profile_invite_code
|
||||
.append(profile_invite_code_table::parse_table_update(table_update)?),
|
||||
@@ -2241,6 +2261,12 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
&self.profile_dashboard_state,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.user_id);
|
||||
diff.profile_feedback_submission = cache
|
||||
.apply_diff_to_table::<ProfileFeedbackSubmission>(
|
||||
"profile_feedback_submission",
|
||||
&self.profile_feedback_submission,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.feedback_id);
|
||||
diff.profile_invite_code = cache
|
||||
.apply_diff_to_table::<ProfileInviteCode>(
|
||||
"profile_invite_code",
|
||||
@@ -2531,6 +2557,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"profile_dashboard_state" => db_update
|
||||
.profile_dashboard_state
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"profile_feedback_submission" => db_update
|
||||
.profile_feedback_submission
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"profile_invite_code" => db_update
|
||||
.profile_invite_code
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
@@ -2757,6 +2786,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"profile_dashboard_state" => db_update
|
||||
.profile_dashboard_state
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"profile_feedback_submission" => db_update
|
||||
.profile_feedback_submission
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"profile_invite_code" => db_update
|
||||
.profile_invite_code
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
@@ -2915,6 +2947,7 @@ pub struct AppliedDiff<'r> {
|
||||
npc_state: __sdk::TableAppliedDiff<'r, NpcState>,
|
||||
player_progression: __sdk::TableAppliedDiff<'r, PlayerProgression>,
|
||||
profile_dashboard_state: __sdk::TableAppliedDiff<'r, ProfileDashboardState>,
|
||||
profile_feedback_submission: __sdk::TableAppliedDiff<'r, ProfileFeedbackSubmission>,
|
||||
profile_invite_code: __sdk::TableAppliedDiff<'r, ProfileInviteCode>,
|
||||
profile_membership: __sdk::TableAppliedDiff<'r, ProfileMembership>,
|
||||
profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>,
|
||||
@@ -3127,6 +3160,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
||||
&self.profile_dashboard_state,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<ProfileFeedbackSubmission>(
|
||||
"profile_feedback_submission",
|
||||
&self.profile_feedback_submission,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<ProfileInviteCode>(
|
||||
"profile_invite_code",
|
||||
&self.profile_invite_code,
|
||||
@@ -3994,6 +4032,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
npc_state_table::register_table(client_cache);
|
||||
player_progression_table::register_table(client_cache);
|
||||
profile_dashboard_state_table::register_table(client_cache);
|
||||
profile_feedback_submission_table::register_table(client_cache);
|
||||
profile_invite_code_table::register_table(client_cache);
|
||||
profile_membership_table::register_table(client_cache);
|
||||
profile_played_world_table::register_table(client_cache);
|
||||
@@ -4067,6 +4106,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
"npc_state",
|
||||
"player_progression",
|
||||
"profile_dashboard_state",
|
||||
"profile_feedback_submission",
|
||||
"profile_invite_code",
|
||||
"profile_membership",
|
||||
"profile_played_world",
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use super::profile_feedback_submission_type::ProfileFeedbackSubmission;
|
||||
use super::runtime_profile_feedback_status_type::RuntimeProfileFeedbackStatus;
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
/// Table handle for the table `profile_feedback_submission`.
|
||||
///
|
||||
/// Obtain a handle from the [`ProfileFeedbackSubmissionTableAccess::profile_feedback_submission`] method on [`super::RemoteTables`],
|
||||
/// like `ctx.db.profile_feedback_submission()`.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.profile_feedback_submission().on_insert(...)`.
|
||||
pub struct ProfileFeedbackSubmissionTableHandle<'ctx> {
|
||||
imp: __sdk::TableHandle<ProfileFeedbackSubmission>,
|
||||
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the table `profile_feedback_submission`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteTables`].
|
||||
pub trait ProfileFeedbackSubmissionTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Obtain a [`ProfileFeedbackSubmissionTableHandle`], which mediates access to the table `profile_feedback_submission`.
|
||||
fn profile_feedback_submission(&self) -> ProfileFeedbackSubmissionTableHandle<'_>;
|
||||
}
|
||||
|
||||
impl ProfileFeedbackSubmissionTableAccess for super::RemoteTables {
|
||||
fn profile_feedback_submission(&self) -> ProfileFeedbackSubmissionTableHandle<'_> {
|
||||
ProfileFeedbackSubmissionTableHandle {
|
||||
imp: self
|
||||
.imp
|
||||
.get_table::<ProfileFeedbackSubmission>("profile_feedback_submission"),
|
||||
ctx: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProfileFeedbackSubmissionInsertCallbackId(__sdk::CallbackId);
|
||||
pub struct ProfileFeedbackSubmissionDeleteCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::Table for ProfileFeedbackSubmissionTableHandle<'ctx> {
|
||||
type Row = ProfileFeedbackSubmission;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = ProfileFeedbackSubmission> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
|
||||
type InsertCallbackId = ProfileFeedbackSubmissionInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> ProfileFeedbackSubmissionInsertCallbackId {
|
||||
ProfileFeedbackSubmissionInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: ProfileFeedbackSubmissionInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
|
||||
type DeleteCallbackId = ProfileFeedbackSubmissionDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> ProfileFeedbackSubmissionDeleteCallbackId {
|
||||
ProfileFeedbackSubmissionDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: ProfileFeedbackSubmissionDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProfileFeedbackSubmissionUpdateCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableWithPrimaryKey for ProfileFeedbackSubmissionTableHandle<'ctx> {
|
||||
type UpdateCallbackId = ProfileFeedbackSubmissionUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> ProfileFeedbackSubmissionUpdateCallbackId {
|
||||
ProfileFeedbackSubmissionUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: ProfileFeedbackSubmissionUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the `feedback_id` unique index on the table `profile_feedback_submission`,
|
||||
/// which allows point queries on the field of the same name
|
||||
/// via the [`ProfileFeedbackSubmissionFeedbackIdUnique::find`] method.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.profile_feedback_submission().feedback_id().find(...)`.
|
||||
pub struct ProfileFeedbackSubmissionFeedbackIdUnique<'ctx> {
|
||||
imp: __sdk::UniqueConstraintHandle<ProfileFeedbackSubmission, String>,
|
||||
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
impl<'ctx> ProfileFeedbackSubmissionTableHandle<'ctx> {
|
||||
/// Get a handle on the `feedback_id` unique index on the table `profile_feedback_submission`.
|
||||
pub fn feedback_id(&self) -> ProfileFeedbackSubmissionFeedbackIdUnique<'ctx> {
|
||||
ProfileFeedbackSubmissionFeedbackIdUnique {
|
||||
imp: self.imp.get_unique_constraint::<String>("feedback_id"),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> ProfileFeedbackSubmissionFeedbackIdUnique<'ctx> {
|
||||
/// Find the subscribed row whose `feedback_id` column value is equal to `col_val`,
|
||||
/// if such a row is present in the client cache.
|
||||
pub fn find(&self, col_val: &String) -> Option<ProfileFeedbackSubmission> {
|
||||
self.imp.find(col_val)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||
let _table =
|
||||
client_cache.get_or_make_table::<ProfileFeedbackSubmission>("profile_feedback_submission");
|
||||
_table.add_unique_constraint::<String>("feedback_id", |row| &row.feedback_id);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn parse_table_update(
|
||||
raw_updates: __ws::v2::TableUpdate,
|
||||
) -> __sdk::Result<__sdk::TableUpdate<ProfileFeedbackSubmission>> {
|
||||
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||
__sdk::InternalError::failed_parse("TableUpdate<ProfileFeedbackSubmission>", "TableUpdate")
|
||||
.with_cause(e)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for query builder access to the table `ProfileFeedbackSubmission`.
|
||||
///
|
||||
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||
pub trait profile_feedback_submissionQueryTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Get a query builder for the table `ProfileFeedbackSubmission`.
|
||||
fn profile_feedback_submission(
|
||||
&self,
|
||||
) -> __sdk::__query_builder::Table<ProfileFeedbackSubmission>;
|
||||
}
|
||||
|
||||
impl profile_feedback_submissionQueryTableAccess for __sdk::QueryTableAccessor {
|
||||
fn profile_feedback_submission(
|
||||
&self,
|
||||
) -> __sdk::__query_builder::Table<ProfileFeedbackSubmission> {
|
||||
__sdk::__query_builder::Table::new("profile_feedback_submission")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_feedback_status_type::RuntimeProfileFeedbackStatus;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct ProfileFeedbackSubmission {
|
||||
pub feedback_id: String,
|
||||
pub user_id: String,
|
||||
pub description: String,
|
||||
pub contact_phone: Option<String>,
|
||||
pub evidence_json: String,
|
||||
pub status: RuntimeProfileFeedbackStatus,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ProfileFeedbackSubmission {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `ProfileFeedbackSubmission`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct ProfileFeedbackSubmissionCols {
|
||||
pub feedback_id: __sdk::__query_builder::Col<ProfileFeedbackSubmission, String>,
|
||||
pub user_id: __sdk::__query_builder::Col<ProfileFeedbackSubmission, String>,
|
||||
pub description: __sdk::__query_builder::Col<ProfileFeedbackSubmission, String>,
|
||||
pub contact_phone: __sdk::__query_builder::Col<ProfileFeedbackSubmission, Option<String>>,
|
||||
pub evidence_json: __sdk::__query_builder::Col<ProfileFeedbackSubmission, String>,
|
||||
pub status:
|
||||
__sdk::__query_builder::Col<ProfileFeedbackSubmission, RuntimeProfileFeedbackStatus>,
|
||||
pub created_at: __sdk::__query_builder::Col<ProfileFeedbackSubmission, __sdk::Timestamp>,
|
||||
pub updated_at: __sdk::__query_builder::Col<ProfileFeedbackSubmission, __sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for ProfileFeedbackSubmission {
|
||||
type Cols = ProfileFeedbackSubmissionCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
ProfileFeedbackSubmissionCols {
|
||||
feedback_id: __sdk::__query_builder::Col::new(table_name, "feedback_id"),
|
||||
user_id: __sdk::__query_builder::Col::new(table_name, "user_id"),
|
||||
description: __sdk::__query_builder::Col::new(table_name, "description"),
|
||||
contact_phone: __sdk::__query_builder::Col::new(table_name, "contact_phone"),
|
||||
evidence_json: __sdk::__query_builder::Col::new(table_name, "evidence_json"),
|
||||
status: __sdk::__query_builder::Col::new(table_name, "status"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed column accessor struct for the table `ProfileFeedbackSubmission`.
|
||||
///
|
||||
/// Provides typed access to indexed columns for query building.
|
||||
pub struct ProfileFeedbackSubmissionIxCols {
|
||||
pub feedback_id: __sdk::__query_builder::IxCol<ProfileFeedbackSubmission, String>,
|
||||
pub user_id: __sdk::__query_builder::IxCol<ProfileFeedbackSubmission, String>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasIxCols for ProfileFeedbackSubmission {
|
||||
type IxCols = ProfileFeedbackSubmissionIxCols;
|
||||
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||
ProfileFeedbackSubmissionIxCols {
|
||||
feedback_id: __sdk::__query_builder::IxCol::new(table_name, "feedback_id"),
|
||||
user_id: __sdk::__query_builder::IxCol::new(table_name, "user_id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::CanBeLookupTable for ProfileFeedbackSubmission {}
|
||||
@@ -34,10 +34,10 @@ pub trait record_daily_login_tracking_event_and_return {
|
||||
input: RuntimeProfileTaskCenterGetInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeTrackingEventProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeTrackingEventProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ impl record_daily_login_tracking_event_and_return for super::RemoteProcedures {
|
||||
input: RuntimeProfileTaskCenterGetInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeTrackingEventProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeTrackingEventProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, RuntimeTrackingEventProcedureResult>(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileFeedbackEvidenceSnapshot {
|
||||
pub evidence_id: String,
|
||||
pub file_name: String,
|
||||
pub content_type: String,
|
||||
pub size_bytes: u64,
|
||||
pub data_url: String,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileFeedbackEvidenceSnapshot {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
pub enum RuntimeProfileFeedbackStatus {
|
||||
Open,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileFeedbackStatus {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_feedback_evidence_snapshot_type::RuntimeProfileFeedbackEvidenceSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileFeedbackSubmissionInput {
|
||||
pub user_id: String,
|
||||
pub description: String,
|
||||
pub contact_phone: Option<String>,
|
||||
pub evidence_items: Vec<RuntimeProfileFeedbackEvidenceSnapshot>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileFeedbackSubmissionInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_feedback_submission_snapshot_type::RuntimeProfileFeedbackSubmissionSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileFeedbackSubmissionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<RuntimeProfileFeedbackSubmissionSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileFeedbackSubmissionProcedureResult {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_feedback_status_type::RuntimeProfileFeedbackStatus;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileFeedbackSubmissionSnapshot {
|
||||
pub feedback_id: String,
|
||||
pub user_id: String,
|
||||
pub description: String,
|
||||
pub contact_phone: Option<String>,
|
||||
pub evidence_json: String,
|
||||
pub status: RuntimeProfileFeedbackStatus,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileFeedbackSubmissionSnapshot {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_feedback_submission_input_type::RuntimeProfileFeedbackSubmissionInput;
|
||||
use super::runtime_profile_feedback_submission_procedure_result_type::RuntimeProfileFeedbackSubmissionProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct SubmitProfileFeedbackAndReturnArgs {
|
||||
pub input: RuntimeProfileFeedbackSubmissionInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for SubmitProfileFeedbackAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `submit_profile_feedback_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait submit_profile_feedback_and_return {
|
||||
fn submit_profile_feedback_and_return(&self, input: RuntimeProfileFeedbackSubmissionInput) {
|
||||
self.submit_profile_feedback_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn submit_profile_feedback_and_return_then(
|
||||
&self,
|
||||
input: RuntimeProfileFeedbackSubmissionInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileFeedbackSubmissionProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl submit_profile_feedback_and_return for super::RemoteProcedures {
|
||||
fn submit_profile_feedback_and_return_then(
|
||||
&self,
|
||||
input: RuntimeProfileFeedbackSubmissionInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileFeedbackSubmissionProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, RuntimeProfileFeedbackSubmissionProcedureResult>(
|
||||
"submit_profile_feedback_and_return",
|
||||
SubmitProfileFeedbackAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -234,6 +234,37 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn submit_profile_feedback(
|
||||
&self,
|
||||
user_id: String,
|
||||
description: String,
|
||||
contact_phone: Option<String>,
|
||||
evidence_items: Vec<module_runtime::RuntimeProfileFeedbackEvidenceSnapshot>,
|
||||
created_at_micros: i64,
|
||||
) -> Result<RuntimeProfileFeedbackSubmissionRecord, SpacetimeClientError> {
|
||||
let procedure_input = build_runtime_profile_feedback_submission_input(
|
||||
user_id,
|
||||
description,
|
||||
contact_phone,
|
||||
evidence_items,
|
||||
created_at_micros,
|
||||
)
|
||||
.map_err(SpacetimeClientError::validation_failed)?
|
||||
.into();
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.submit_profile_feedback_and_return_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_runtime_profile_feedback_submission_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_profile_referral_invite_center(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -385,14 +416,15 @@ impl SpacetimeClient {
|
||||
.into();
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.query_analytics_metric_then(procedure_input, move |_, result| {
|
||||
connection.procedures().query_analytics_metric_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_analytics_metric_query_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@ macro_rules! migration_tables {
|
||||
public_work_like,
|
||||
profile_membership,
|
||||
profile_recharge_order,
|
||||
profile_feedback_submission,
|
||||
profile_save_archive,
|
||||
player_progression,
|
||||
chapter_progression,
|
||||
|
||||
@@ -351,6 +351,27 @@ pub struct ProfileRechargeOrder {
|
||||
pub(crate) membership_expires_at: Option<Timestamp>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = profile_feedback_submission,
|
||||
index(accessor = by_profile_feedback_user_id, btree(columns = [user_id])),
|
||||
index(
|
||||
accessor = by_profile_feedback_user_created_at,
|
||||
btree(columns = [user_id, created_at])
|
||||
)
|
||||
)]
|
||||
pub struct ProfileFeedbackSubmission {
|
||||
#[primary_key]
|
||||
pub(crate) feedback_id: String,
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) description: String,
|
||||
pub(crate) contact_phone: Option<String>,
|
||||
// 中文注释:首版凭证以 Data URL 写入私有表,HTTP 回包只返回元数据,后续迁 OSS 不改变外部契约。
|
||||
pub(crate) evidence_json: String,
|
||||
pub(crate) status: RuntimeProfileFeedbackStatus,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = profile_save_archive,
|
||||
index(accessor = by_profile_save_archive_user_id, btree(columns = [user_id])),
|
||||
@@ -749,6 +770,25 @@ pub fn create_profile_recharge_order_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn submit_profile_feedback_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileFeedbackSubmissionInput,
|
||||
) -> RuntimeProfileFeedbackSubmissionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| submit_profile_feedback_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeProfileFeedbackSubmissionProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileFeedbackSubmissionProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请中心会在首次打开时为账号创建稳定邀请码,前端只展示这里返回的后端状态。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_profile_referral_invite_center(
|
||||
@@ -1906,6 +1946,47 @@ fn create_profile_recharge_order_record(
|
||||
))
|
||||
}
|
||||
|
||||
fn submit_profile_feedback_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileFeedbackSubmissionInput,
|
||||
) -> Result<RuntimeProfileFeedbackSubmissionSnapshot, String> {
|
||||
let validated_input = build_runtime_profile_feedback_submission_input(
|
||||
input.user_id,
|
||||
input.description,
|
||||
input.contact_phone,
|
||||
input.evidence_items,
|
||||
input.created_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let created_at = Timestamp::from_micros_since_unix_epoch(validated_input.created_at_micros);
|
||||
let feedback_id = build_runtime_profile_feedback_submission_id(
|
||||
&validated_input.user_id,
|
||||
validated_input.created_at_micros,
|
||||
);
|
||||
let evidence_json = serde_json::to_string(&validated_input.evidence_items)
|
||||
.map_err(|error| format!("反馈凭证序列化失败: {error}"))?;
|
||||
let row = ProfileFeedbackSubmission {
|
||||
feedback_id: feedback_id.clone(),
|
||||
user_id: validated_input.user_id,
|
||||
description: validated_input.description,
|
||||
contact_phone: validated_input.contact_phone,
|
||||
evidence_json,
|
||||
status: RuntimeProfileFeedbackStatus::Open,
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
};
|
||||
ctx.db.profile_feedback_submission().insert(row);
|
||||
|
||||
let latest = ctx
|
||||
.db
|
||||
.profile_feedback_submission()
|
||||
.feedback_id()
|
||||
.find(&feedback_id)
|
||||
.ok_or_else(|| "profile_feedback_submission 写入后未能读取".to_string())?;
|
||||
|
||||
Ok(build_profile_feedback_submission_snapshot_from_row(&latest))
|
||||
}
|
||||
|
||||
fn get_profile_referral_invite_center_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeReferralInviteCenterGetInput,
|
||||
@@ -3440,6 +3521,21 @@ fn build_profile_recharge_order_snapshot_from_row(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_feedback_submission_snapshot_from_row(
|
||||
row: &ProfileFeedbackSubmission,
|
||||
) -> RuntimeProfileFeedbackSubmissionSnapshot {
|
||||
RuntimeProfileFeedbackSubmissionSnapshot {
|
||||
feedback_id: row.feedback_id.clone(),
|
||||
user_id: row.user_id.clone(),
|
||||
description: row.description.clone(),
|
||||
contact_phone: row.contact_phone.clone(),
|
||||
evidence_json: row.evidence_json.clone(),
|
||||
status: row.status,
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_played_world_snapshot_from_row(
|
||||
row: &ProfilePlayedWorld,
|
||||
) -> RuntimeProfilePlayedWorldSnapshot {
|
||||
|
||||
Reference in New Issue
Block a user