feat: add wechat mini program virtual payment

This commit is contained in:
kdletters
2026-05-31 22:44:22 +08:00
parent 78448d2a7b
commit 3db956ec81
24 changed files with 919 additions and 99 deletions
+2
View File
@@ -109,6 +109,8 @@ WECHAT_MOCK_USER_ID="wx-mock-user"
WECHAT_MOCK_UNION_ID="wx-mock-union"
WECHAT_MOCK_DISPLAY_NAME="微信旅人"
WECHAT_MOCK_AVATAR_URL=""
WECHAT_MINIPROGRAM_MESSAGE_TOKEN=""
WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=""
# Model name for chat completions.
VITE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
+2
View File
@@ -39,3 +39,5 @@ GENARRATIVE_LLM_PROVIDER=openai-compatible
GENARRATIVE_LLM_BASE_URL=
GENARRATIVE_LLM_API_KEY=
GENARRATIVE_LLM_MODEL=
WECHAT_MINIPROGRAM_MESSAGE_TOKEN=
WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=
+2
View File
@@ -109,6 +109,8 @@ WECHAT_AUTHORIZE_ENDPOINT=https://open.weixin.qq.com/connect/qrconnect
WECHAT_ACCESS_TOKEN_ENDPOINT=https://api.weixin.qq.com/sns/oauth2/access_token
WECHAT_USER_INFO_ENDPOINT=https://api.weixin.qq.com/sns/userinfo
WECHAT_STATE_TTL_MINUTES=15
WECHAT_MINIPROGRAM_MESSAGE_TOKEN=
WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=
ALIYUN_OSS_BUCKET=
ALIYUN_OSS_ENDPOINT=oss-cn-shanghai.aliyuncs.com
@@ -8,7 +8,7 @@
- 会员商品在微信小程序 WebView 内同样走 `wechat_mp_virtual`,由小程序页调用 `wx.requestVirtualPayment``short_series_goods` 模式,并在 `signData` 内带 `productId``goodsPrice`
- H5 与桌面微信环境仍分别走 `wechat_h5` / `wechat_native`,不进入虚拟支付链路。
- `session_key` 只保存在后端认证仓储内,用于计算虚拟支付用户态签名,不下发给前端。
- 客户端支付成功回调只代表已拉起支付并返回成功;最终到账仍以后端微信通知或查询确认后写入订单为准
- 客户端支付成功回调只代表已拉起支付并返回成功;最终到账仍以后端虚拟支付消息推送写入订单为准,普通微信支付订单则继续走微信支付 V3 notify / query
- 小程序 WebView 默认进入时会静默调用 `wx.login` 刷新后端微信登录态,避免历史登录用户只有前端 JWT、后端缺少 `session_key` 时无法生成虚拟支付签名。
## 关键文件
@@ -30,6 +30,8 @@ WECHAT_PAY_PROVIDER=real
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID=<微信虚拟支付 offerId>
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY=<现网 AppKey>
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY=<沙箱 AppKey,可选>
WECHAT_MINIPROGRAM_MESSAGE_TOKEN=<微信消息推送 Token>
WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=<微信消息推送 EncodingAESKey>
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV=0
```
@@ -40,6 +42,9 @@ WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV=0
- `signature``HMAC-SHA256(session_key, signData)` 的小写 hex。
- 泥点属于微信虚拟支付代币(coin),`short_series_coin``buyQuantity` 必须使用当前泥点商品的 `points_amount`;例如 60 泥点商品应传 `buyQuantity: 60`
- 会员直购 `signData` 额外包含 `productId``goodsPrice``goodsPrice` 使用后端商品配置价,和微信后台道具价格校验保持一致。
- 微信小程序“开发者服务器接收消息推送”必须配置为安全模式,数据格式选 JSON,URL 统一指向 `/api/profile/recharge/wechat/virtual-notify`
- `WECHAT_MINIPROGRAM_MESSAGE_TOKEN``WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY` 由环境变量注入;后端会先校验 `signature/msg_signature`,再用 `EncodingAESKey` 解密 `Encrypt`,然后按虚拟支付事件入账。
- 安全模式下,GET 验证会直接返回解密后的 `echostr`POST 推送会先解密再解析 `xpay_goods_deliver_notify` / `xpay_coin_pay_notify`
## 验收命令
@@ -59,5 +64,6 @@ npm run check:encoding
- 后台新增的会员类充值商品会直接把商品 `productId` 作为微信 `short_series_goods` 的道具 ID;例如微信后台道具 ID 为 `item01` 时,后台会员商品 `productId` 也应配置为 `item01`,且商品价格需要与微信后台道具价格一致。
- 小程序页必须保留普通支付与虚拟支付双分支,按 pay params 字段判断调用 `wx.requestPayment``wx.requestVirtualPayment`
- 小程序支付承接页回传 `wx_pay_result` 时必须携带 `requestId:status:orderId[:error]`,并同时写入上一页 hash 与本地 storageWebView `onShow` 会立即检查一次、延迟二次检查一次,且同名 hash 参数必须替换,避免支付状态停留在处理中或重复处理。
- 微信虚拟支付消息推送使用独立后端入口 `/api/profile/recharge/wechat/virtual-notify`,按 `xpay_goods_deliver_notify``xpay_coin_pay_notify` 推进充值订单入账;回包需按入站格式返回 `ErrCode=0` / `ErrMsg=success`JSON 入站回 JSON,XML 入站回 XML),错误时带具体 `ErrMsg` 便于微信侧重试与排障。
- 沙箱或基础库失败会把微信返回的 `errCode` / `errMsg` 透传到前端失败弹窗,便于区分微信后台道具、沙箱 AppKey、签名和基础库能力问题。
- Web 侧在“正在支付”状态下会短时轮询 `wx_pay_result`,即使小程序 `web-view` 回写 hash 没触发浏览器 `hashchange`,也必须展示回写的微信错误内容。
+2 -2
View File
@@ -1,11 +1,11 @@
// 中文注释:这里填写已经在“小程序后台-开发-开发设置-业务域名”配置过的 H5 入口。
// 示例:https://game.example.com/
// 注意:必须是 https 域名,不能是 localhost、IP 地址或未备案域名。
const WEB_VIEW_ENTRY_URL = 'https://www.genarrative.world/';
const WEB_VIEW_ENTRY_URL = 'https://outfield-outlook-reprocess.ngrok-free.dev';
// 中文注释:这里填写 Rust api-server 的公网 HTTPS 域名,必须在“小程序后台-开发设置-request 合法域名”中配置。
// 如果 H5 和 API 同域,可保持和 WEB_VIEW_ENTRY_URL 同一个域名;请求路径会固定走 /api/auth/wechat/miniprogram-login。
const API_BASE_URL = 'https://www.genarrative.world/';
const API_BASE_URL = 'https://outfield-outlook-reprocess.ngrok-free.dev';
// 中文注释:这里填写微信小程序 AppID,用于后端记录会话来源;project.config.json 里的 appid 也要保持一致。
const MINI_PROGRAM_APP_ID = 'wx3da23ea14ca66b65';
+53
View File
@@ -17,6 +17,17 @@ dependencies = [
"pom",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures 0.2.17",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -78,12 +89,15 @@ checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25"
name = "api-server"
version = "0.1.0"
dependencies = [
"aes",
"async-stream",
"axum",
"base64 0.22.1",
"bytes",
"cbc",
"dotenvy",
"futures-util",
"hex",
"hmac",
"http-body-util",
"image",
@@ -118,6 +132,7 @@ dependencies = [
"ring",
"serde",
"serde_json",
"sha1",
"sha2",
"shared-contracts",
"shared-kernel",
@@ -351,6 +366,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "3.5.0"
@@ -415,6 +439,15 @@ dependencies = [
"rustversion",
]
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.2.60"
@@ -453,6 +486,16 @@ dependencies = [
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "console"
version = "0.16.3"
@@ -1461,6 +1504,16 @@ dependencies = [
"serde_core",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "insta"
version = "1.47.2"
+4
View File
@@ -89,16 +89,19 @@ shared-logging = { path = "crates/shared-logging", default-features = false }
spacetime-client = { path = "crates/spacetime-client", default-features = false }
argon2 = "0.5"
aes = "0.8"
async-stream = "0.3"
async-trait = "0.1"
axum = "0.8"
base64 = "0.22"
cbc = { version = "0.1", features = ["alloc"] }
bytes = "1"
dotenvy = "0.15"
flate2 = "1"
futures-util = "0.3"
hmac = "0.12"
http-body-util = "0.1"
hex = "0.4"
image = { version = "0.25", default-features = false }
jsonwebtoken = "9"
langchainrust = "0.2.18"
@@ -109,6 +112,7 @@ ring = "0.17"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_urlencoded = "0.7"
sha1 = "0.10"
sha2 = "0.10"
socket2 = "0.6"
spacetimedb = "2.2.0"
+4
View File
@@ -5,11 +5,14 @@ version.workspace = true
license.workspace = true
[dependencies]
aes = { workspace = true }
async-stream = { workspace = true }
axum = { workspace = true, features = ["ws"] }
base64 = { workspace = true }
cbc = { workspace = true }
bytes = { workspace = true }
dotenvy = { workspace = true }
hex = { workspace = true }
image = { workspace = true, features = ["jpeg", "png", "webp"] }
http-body-util = { workspace = true }
reqwest = { workspace = true, features = ["json", "multipart", "rustls-tls"] }
@@ -44,6 +47,7 @@ hmac = { workspace = true }
ring = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
shared-contracts = { workspace = true, features = ["oss-contracts"] }
shared-kernel = { workspace = true }
+9 -1
View File
@@ -41,7 +41,10 @@ use crate::{
start_visual_novel_run, stream_visual_novel_action, stream_visual_novel_message,
submit_visual_novel_message, update_visual_novel_work,
},
wechat_pay::handle_wechat_pay_notify,
wechat_pay::{
handle_wechat_pay_notify, handle_wechat_virtual_payment_message_push_verify,
handle_wechat_virtual_payment_notify,
},
};
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
@@ -70,6 +73,11 @@ pub fn build_router(state: AppState) -> Router {
"/api/profile/recharge/wechat/notify",
post(handle_wechat_pay_notify),
)
.route(
"/api/profile/recharge/wechat/virtual-notify",
get(handle_wechat_virtual_payment_message_push_verify)
.post(handle_wechat_virtual_payment_notify),
)
.route(
"/api/runtime/sessions/{runtime_session_id}/inventory",
get(get_runtime_inventory_state).route_layer(middleware::from_fn_with_state(
@@ -94,13 +94,11 @@ pub async fn generate_character_visual(
.map_err(|error| character_visual_error_response(&request_context, error))?;
let result = async {
let settings = require_openai_image_settings(&state)?
.with_external_api_audit_context(
&request_context,
Some(owner_user_id.clone()),
Some(character_id.clone()),
)
;
let settings = require_openai_image_settings(&state)?.with_external_api_audit_context(
&request_context,
Some(owner_user_id.clone()),
Some(character_id.clone()),
);
let http_client = build_openai_image_http_client(&settings)?;
state
@@ -324,10 +322,8 @@ pub(crate) async fn generate_character_primary_visual_for_profile(
&model,
&prompt,
)?;
let settings = require_openai_image_settings(state)?.with_external_api_audit_actor(
Some(owner_user_id.to_string()),
Some(character_id.clone()),
);
let settings = require_openai_image_settings(state)?
.with_external_api_audit_actor(Some(owner_user_id.to_string()), Some(character_id.clone()));
let http_client = build_openai_image_http_client(&settings)?;
state
.ai_task_service()
+36 -12
View File
@@ -97,6 +97,8 @@ pub struct AppConfig {
pub wechat_mini_program_virtual_payment_offer_id: Option<String>,
pub wechat_mini_program_virtual_payment_app_key: Option<String>,
pub wechat_mini_program_virtual_payment_sandbox_app_key: Option<String>,
pub wechat_mini_program_message_token: Option<String>,
pub wechat_mini_program_message_encoding_aes_key: Option<String>,
pub wechat_mini_program_virtual_payment_env: u8,
pub oss_bucket: Option<String>,
pub oss_endpoint: Option<String>,
@@ -244,6 +246,8 @@ impl Default for AppConfig {
wechat_mini_program_virtual_payment_offer_id: None,
wechat_mini_program_virtual_payment_app_key: None,
wechat_mini_program_virtual_payment_sandbox_app_key: None,
wechat_mini_program_message_token: None,
wechat_mini_program_message_encoding_aes_key: None,
wechat_mini_program_virtual_payment_env: 0,
oss_bucket: None,
oss_endpoint: None,
@@ -598,8 +602,11 @@ impl AppConfig {
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY"]);
config.wechat_mini_program_virtual_payment_sandbox_app_key =
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY"]);
if let Some(env) =
read_first_u8_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV"])
config.wechat_mini_program_message_token =
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_MESSAGE_TOKEN"]);
config.wechat_mini_program_message_encoding_aes_key =
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY"]);
if let Some(env) = read_first_u8_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV"])
&& env <= 1
{
config.wechat_mini_program_virtual_payment_env = env;
@@ -1396,6 +1403,8 @@ mod tests {
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY");
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN");
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV");
std::env::set_var("WECHAT_PAY_ENABLED", "true");
std::env::set_var("WECHAT_PAY_PROVIDER", "real");
@@ -1412,18 +1421,17 @@ mod tests {
"WECHAT_PAY_NOTIFY_URL",
"https://api.example.com/api/profile/recharge/wechat/notify",
);
std::env::set_var(
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID",
"offer-001",
);
std::env::set_var(
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY",
"app-key-001",
);
std::env::set_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID", "offer-001");
std::env::set_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY", "app-key-001");
std::env::set_var(
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY",
"sandbox-app-key-001",
);
std::env::set_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN", "message-token-001");
std::env::set_var(
"WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY",
"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
);
std::env::set_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV", "1");
}
@@ -1448,13 +1456,27 @@ mod tests {
Some("platform-serial-001")
);
assert_eq!(
config.wechat_mini_program_virtual_payment_offer_id.as_deref(),
config
.wechat_mini_program_virtual_payment_offer_id
.as_deref(),
Some("offer-001")
);
assert_eq!(
config.wechat_mini_program_virtual_payment_app_key.as_deref(),
config
.wechat_mini_program_virtual_payment_app_key
.as_deref(),
Some("app-key-001")
);
assert_eq!(
config.wechat_mini_program_message_token.as_deref(),
Some("message-token-001")
);
assert_eq!(
config
.wechat_mini_program_message_encoding_aes_key
.as_deref(),
Some("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG")
);
assert_eq!(
config
.wechat_mini_program_virtual_payment_sandbox_app_key
@@ -1476,6 +1498,8 @@ mod tests {
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY");
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN");
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV");
}
}
@@ -553,12 +553,11 @@ pub async fn generate_custom_world_scene_image(
"scene_image",
asset_id.as_str(),
async {
let settings = require_openai_image_settings(&state)?
.with_external_api_audit_context(
&request_context,
Some(owner_user_id.to_string()),
normalized.profile_id.clone(),
);
let settings = require_openai_image_settings(&state)?.with_external_api_audit_context(
&request_context,
Some(owner_user_id.to_string()),
normalized.profile_id.clone(),
);
let http_client = build_openai_image_http_client(&settings)?;
let reference_image =
if let Some(reference_image_src) = normalized.reference_image_src.as_deref() {
@@ -1052,6 +1052,7 @@ mod tests {
external_api_audit_state: None,
external_api_audit_user_id: None,
external_api_audit_profile_id: None,
external_api_audit_request_id: None,
});
assert_eq!(
+5 -6
View File
@@ -414,12 +414,11 @@ async fn maybe_generate_jump_hop_assets(
let settings = require_openai_image_settings(state)
.map(|settings| {
settings
.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.clone()),
)
settings.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.clone()),
)
})
.map_err(|error| {
jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error)
@@ -755,12 +755,11 @@ async fn generate_match3d_material_sheet_from_level_scene(
config: &Match3DConfigJson,
background_asset: Option<&Match3DGeneratedBackgroundAsset>,
) -> Result<Match3DMaterialSheet, AppError> {
let settings = require_openai_image_settings(state)?
.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?;
let prompt = build_match3d_item_spritesheet_prompt();
let reference = load_match3d_level_scene_reference_image(state, background_asset).await?;
@@ -304,12 +304,11 @@ pub(super) async fn generate_match3d_cover_image_asset(
reference_image_srcs: Vec<String>,
) -> Result<Match3DAssetUpload, AppError> {
require_match3d_oss_client(state)?;
let settings = require_openai_image_settings(state)?
.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?;
let cover_prompt = build_match3d_cover_generation_prompt(config, prompt);
let generated = if let Some(uploaded_image) = resolve_match3d_reference_image_for_edit(
@@ -459,12 +458,11 @@ pub(super) async fn generate_match3d_level_asset_bundle(
prompt: &str,
) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
require_match3d_oss_client(state)?;
let settings = require_openai_image_settings(state)?
.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?;
let level_scene_prompt = build_match3d_level_scene_generation_prompt(config);
@@ -607,12 +605,11 @@ pub(super) async fn generate_match3d_container_image(
prompt: &str,
) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
require_match3d_oss_client(state)?;
let settings = require_openai_image_settings(state)?
.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?;
let reference_image = load_match3d_container_reference_image()?;
let container_prompt = build_match3d_container_generation_prompt(config, prompt);
@@ -310,12 +310,11 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
level_name: &str,
puzzle_image: &PuzzleDownloadedImage,
) -> Result<GeneratedPuzzleLevelAssetBundle, AppError> {
let settings = require_puzzle_vector_engine_settings(state)?
.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(session_id.to_string()),
);
let settings = require_puzzle_vector_engine_settings(state)?.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(session_id.to_string()),
);
let http_client = build_puzzle_image_http_client(state, PuzzleImageModel::GptImage2)?;
let puzzle_reference = build_puzzle_downloaded_image_reference(puzzle_image);
let scene_generated = create_puzzle_vector_engine_image_generation(
@@ -117,11 +117,9 @@ impl PuzzleVectorEngineSettings {
) -> Self {
self.external_api_audit_user_id = user_id;
self.external_api_audit_profile_id = profile_id;
self.external_api_audit_request_id =
Some(request_context.request_id().to_string());
self.external_api_audit_request_id = Some(request_context.request_id().to_string());
self
}
}
pub(crate) struct ParsedPuzzleImageDataUrl {
@@ -1240,8 +1240,7 @@ fn build_wechat_virtual_pay_params(
}
let sign_data = sign_data.to_string();
let pay_sig = calc_wechat_virtual_payment_signature(state, &sign_data, false)?;
let signature =
calc_wechat_virtual_payment_user_signature_with_key(&session_key, &sign_data)?;
let signature = calc_wechat_virtual_payment_user_signature_with_key(&session_key, &sign_data)?;
Ok(WechatMiniProgramVirtualPayParamsResponse {
mode: mode.to_string(),
@@ -2342,8 +2341,9 @@ mod tests {
has_points_recharged: false,
};
let params = build_wechat_virtual_pay_params(&state, &center, &order, "openid-user-00000001")
.expect("membership virtual pay params should build");
let params =
build_wechat_virtual_pay_params(&state, &center, &order, "openid-user-00000001")
.expect("membership virtual pay params should build");
let sign_data: Value =
serde_json::from_str(&params.sign_data).expect("sign data should be valid json");
let attach: Value = serde_json::from_str(
@@ -2439,13 +2439,9 @@ mod tests {
has_points_recharged: true,
};
let params = build_wechat_virtual_pay_params(
&state,
&center,
&order,
"openid-user-points-60",
)
.expect("points virtual pay params should build");
let params =
build_wechat_virtual_pay_params(&state, &center, &order, "openid-user-points-60")
.expect("points virtual pay params should build");
let sign_data: Value =
serde_json::from_str(&params.sign_data).expect("sign data should be valid json");
let attach: Value = serde_json::from_str(
@@ -2554,9 +2550,8 @@ mod tests {
fn wechat_virtual_payment_signatures_match_official_examples() {
let post_body = r#"{"openid": "xxx", "user_ip": "127.0.0.1", "env": 0}"#;
let pay_sig =
calc_wechat_virtual_payment_pay_signature_with_key("12345", post_body)
.expect("pay signature should build");
let pay_sig = calc_wechat_virtual_payment_pay_signature_with_key("12345", post_body)
.expect("pay signature should build");
let signature = calc_wechat_virtual_payment_user_signature_with_key(
"9hAb/NEYUlkaMBEsmFgzig==",
post_body,
@@ -398,12 +398,11 @@ async fn generate_square_hole_image_data_url(
size: &str,
failure_context: &str,
) -> Result<String, AppError> {
let settings = require_openai_image_settings(state)?
.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?;
let generated = create_openai_image_generation(
&http_client,
File diff suppressed because it is too large Load Diff
@@ -1461,8 +1461,10 @@ mod tests {
assert_eq!(asset.prompt.as_deref(), Some("默认木鱼音"));
}
#[test]
fn wooden_fish_draft_uses_default_hit_sound_asset_and_ignores_prompt() {
#[tokio::test]
async fn wooden_fish_draft_uses_default_hit_sound_asset_and_ignores_prompt() {
let state = crate::state::AppState::new(crate::config::AppConfig::default())
.expect("state should build");
let payload = WoodenFishWorkspaceCreateRequest {
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
work_title: "今日敲木鱼".to_string(),
@@ -1475,7 +1477,9 @@ mod tests {
floating_words: vec![],
};
let draft = build_wooden_fish_draft(&payload);
let draft = build_wooden_fish_draft(&payload, &state)
.await
.expect("draft should build");
assert!(draft.hit_sound_prompt.is_none());
let asset = draft
@@ -1524,8 +1524,14 @@ mod tests {
payload["wechatMiniProgramPayParams"]["signData"],
json!("{\"offerId\":\"offer-1\",\"productId\":\"member_month\",\"goodsPrice\":2800}")
);
assert_eq!(payload["wechatMiniProgramPayParams"]["paySig"], json!("pay-sig"));
assert_eq!(payload["wechatMiniProgramPayParams"]["signature"], json!("user-sig"));
assert_eq!(
payload["wechatMiniProgramPayParams"]["paySig"],
json!("pay-sig")
);
assert_eq!(
payload["wechatMiniProgramPayParams"]["signature"],
json!("user-sig")
);
}
#[test]
+1
View File
@@ -61,6 +61,7 @@ export default defineConfig(({mode}) => {
chunkSizeWarningLimit: 1000,
},
server: {
allowedHosts: ["outfield-outlook-reprocess.ngrok-free.dev"],
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify; file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',