Skip to main content

wx_rust_open/api/impl/
wx_open_component_service_impl.rs

1//! 组件服务实现。
2//!
3//! 对应 Java `me.chanjar.weixin.open.api.impl.WxOpenComponentServiceImpl`:
4//! 持有门面服务引用(Java 强引用构造 `new WxOpenComponentServiceImpl(this)`,
5//! Rust 以 `Weak<dyn WxOpenService>` 打破循环),实现
6//! component_access_token 双检锁刷新、带 token 注入的 get/post、
7//! verify ticket 推送启动、预授权码与预授权链接,以及 Wave 2 补齐的
8//! 授权方信息/选项/列表、代码模板、open 帐号、快速创建、minishop、tcb、
9//! oauth2、服务器域名等全部接口方法(Wave 2 待定清单见各方法注释)。
10//!
11//! 镜像约定:
12//! - Java `getWxOpenService().post(url, json)`(裸 post,URL 已带
13//!   access_token 或无需 token)→ [`Self::bare_post`](直调执行器,不做
14//!   token 注入);Java 组件层 `post(url, json)` → [`Self::post`]。
15//! - minishop 入参 bean 的 Java `toJsonObject()` 均为手工拼装 snake_case
16//!   JSON,与 Rust bean 的 serde(camelCase)不一致,故在
17//!   [`minishop_json`] 中以 `serde_json::json!` 逐键镜像 Java 线格式。
18//! - Java `WxOpenMinishopService.UPLOAD_IMG_MINISHOP_FILE_URL` 与接口常量
19//!   `UPLOAD_IMAGE_URL` 同值,统一为 [`urls::upload_image_url`]。
20//! - 未实现(待依赖接线)清单:`getWxMpServiceByAppid`/
21//!   `getWxMaServiceByAppid`/`getWxFastMaServiceByAppid`/
22//!   `getWxMinishopServiceByAppid`(恒 None)与依赖其 post 的
23//!   `createOpenAccount`/`bindOpenAccount`/`unbindOpenAccount`/
24//!   `getOpenAccount`(Err(-99));Java 恒 `return null` 的
25//!   `minishopGetCouponList`/`minishopCommonPost` 以 `Ok(None)` 镜像。
26//!
27//! Wave 4 更新(代 mp/ma 桥接接线,对应本文件
28//! [`WxOpenComponentServiceImpl`]):
29//! - `getWxMpServiceByAppid`/`getWxMaServiceByAppid`/
30//!   `getWxFastMaServiceByAppid` 由恒 None 改为按 appId 双检锁缓存装配
31//!   [`crate::api::r#impl::WxOpenMpService`] /
32//!   [`crate::api::r#impl::WxOpenMaService`](镜像 Java 静态
33//!   ConcurrentHashMap + synchronized 双检锁;Rust 以实例级
34//!   `Mutex<HashMap>` 表达),返回值仍为 `Arc<dyn Any + Send + Sync>`,
35//!   调用方经 `downcast_mp_service`/`downcast_ma_service` 下转;
36//! - `createOpenAccount`/`bindOpenAccount`/`unbindOpenAccount`/
37//!   `getOpenAccount` 解锁:镜像 Java 私有方法 `openAccountServicePost`
38//!   (switch appIdType:mp → 代 mp 服务 post;mini → 代 ma 服务 post;
39//!   其余抛「appIdType类型异常」);
40//! - `getWxMinishopServiceByAppid` 接线(Wave 5):双检锁缓存装配
41//!   [`WxOpenMinishopServiceImpl`](此前恒 None)。
42//! - Ma*/Minishop 子域服务(Wave 5)见 `api/wx_open_ma_*_service.rs` /
43//!   `api/impl/wx_open_ma_*_service_impl.rs`,由代 ma 桥接
44//!   [`WxOpenMaService`] 按 Java 语义装配七个子服务;组件层
45//!   `getWxMaServiceByAppid` 取回的桥接实例自带全部子服务 getter。
46
47use std::any::Any;
48use std::collections::HashMap;
49use std::sync::{Arc, Mutex, Weak};
50use std::time::Duration;
51
52use async_trait::async_trait;
53use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
54
55use wx_rust_common::api::wx_consts::app_id_type;
56use wx_rust_common::bean::oauth2::WxOAuth2AccessToken;
57use wx_rust_common::bean::result::WxMinishopImageUploadResult;
58use wx_rust_common::enums::WxType;
59use wx_rust_common::error::WxErrorException;
60use wx_rust_common::util::http::{
61    RequestExecutor, SimpleGetRequestExecutor, SimplePostRequestExecutor,
62};
63use wx_rust_miniapp::api::WxMaService;
64use wx_rust_mp::api::WxMpService;
65
66use crate::api::r#impl::base_wx_open_service_impl;
67use crate::api::r#impl::{WxOpenMaService, WxOpenMinishopServiceImpl, WxOpenMpService};
68use crate::api::{WxOpenComponentService, WxOpenService};
69use crate::bean::message::WxOpenXmlMessage;
70use crate::bean::{
71    GetShareCloudBaseEnvResponse, GetTcbEnvListResponse, LimitDiscountGoods, LimitDiscountSku,
72    MinishopBrand, MinishopBrandList, MinishopBusiLicense, MinishopCategories, MinishopCategory,
73    MinishopDeliveryTemplate, MinishopDeliveryTemplateResult, MinishopIdcardInfo, MinishopNameInfo,
74    MinishopOrganizationCodeInfo, MinishopReturnInfo, MinishopShopCat, MinishopShopCatList,
75    MinishopSuperAdministratorInfo, ShareCloudBaseEnvRequest, ShareCloudBaseEnvResponse,
76    ValuationType, WxMinishopAddGoodsSpuResult, WxMinishopCoupon, WxMinishopCouponStock,
77    WxMinishopSku, WxMinishopSpu, WxOpenAuthorizerAccessToken, WxOpenAuthorizerInfoResult,
78    WxOpenAuthorizerListResult, WxOpenAuthorizerOptionResult, WxOpenCreateResult, WxOpenGetResult,
79    WxOpenHaveResult, WxOpenMaApplyOrderPathInfo, WxOpenMaCodeTemplate,
80    WxOpenMaDomainConfirmFileResult, WxOpenMaDomainResult, WxOpenMaWebDomainResult,
81    WxOpenQueryAuthResult, WxOpenRegisterBetaWeappResult, WxOpenRegisterPersonalWeappResult,
82    WxOpenResult,
83};
84use crate::config::WxOpenConfigStorage;
85use crate::constant::wx_open_constants::ACCESS_TOKEN_KEY_COMPONENT;
86use crate::enums::url_core::{
87    api_authorizer_token_url, api_create_preauthcode_url, api_start_push_ticket_url,
88    component_login_page_url, component_mobile_login_page_url,
89};
90
91// ---------------------------------------------------------------------------
92// URL 常量(对应 Java `WxOpenComponentService` 接口常量;以
93// `WxOpenHostConfig.api_host` 前缀拼接,与 url_core 同一模式;limit
94// discount 三个 URL 在 Java 中直接拼接 "access_token="(无 "?"),镜像)
95// ---------------------------------------------------------------------------
96mod urls {
97    use crate::config::{API_DEFAULT_HOST_URL, WxOpenConfigStorage};
98
99    /// 生成完整接口地址:域名前缀 + 路径(对应 Java 常量中的
100    /// `https://api.weixin.qq.com` 前缀,支持自定义 apiHostUrl 替换)。
101    pub(super) fn api(config: &dyn WxOpenConfigStorage, path: &str) -> String {
102        let host = config
103            .wx_open_host_config()
104            .map(|h| h.api_host)
105            .unwrap_or_else(|| API_DEFAULT_HOST_URL.to_string());
106        format!("{host}{path}")
107    }
108
109    pub(super) fn query_auth(config: &dyn WxOpenConfigStorage) -> String {
110        api(config, "/cgi-bin/component/api_query_auth")
111    }
112    pub(super) fn get_authorizer_info(config: &dyn WxOpenConfigStorage) -> String {
113        api(config, "/cgi-bin/component/api_get_authorizer_info")
114    }
115    pub(super) fn get_authorizer_option(config: &dyn WxOpenConfigStorage) -> String {
116        api(config, "/cgi-bin/component/get_authorizer_option")
117    }
118    pub(super) fn set_authorizer_option(config: &dyn WxOpenConfigStorage) -> String {
119        api(config, "/cgi-bin/component/set_authorizer_option")
120    }
121    pub(super) fn get_authorizer_list(config: &dyn WxOpenConfigStorage) -> String {
122        api(config, "/cgi-bin/component/api_get_authorizer_list")
123    }
124    /// oauth2 access_token(对应 Java `OAUTH2_ACCESS_TOKEN_URL`,%s 由调用方
125    /// format;Java 不编码 appid/code,镜像)。
126    pub(super) fn oauth2_access_token(
127        config: &dyn WxOpenConfigStorage,
128        app_id: &str,
129        code: &str,
130    ) -> String {
131        format!(
132            "{}/sns/oauth2/component/access_token?appid={}&code={}&grant_type=authorization_code&component_appid={}",
133            host(config),
134            app_id,
135            code,
136            component_app_id(config),
137        )
138    }
139    /// oauth2 refresh_token(对应 Java `OAUTH2_REFRESH_TOKEN_URL`)。
140    pub(super) fn oauth2_refresh_token(
141        config: &dyn WxOpenConfigStorage,
142        app_id: &str,
143        refresh_token: &str,
144    ) -> String {
145        format!(
146            "{}/sns/oauth2/component/refresh_token?appid={}&grant_type=refresh_token&refresh_token={}&component_appid={}",
147            host(config),
148            app_id,
149            refresh_token,
150            component_app_id(config),
151        )
152    }
153    /// 小程序 code 换 session(对应 Java `MINIAPP_JSCODE_2_SESSION`)。
154    pub(super) fn miniapp_jscode2_session(
155        config: &dyn WxOpenConfigStorage,
156        app_id: &str,
157        js_code: &str,
158    ) -> String {
159        format!(
160            "{}/sns/component/jscode2session?appid={}&js_code={}&grant_type=authorization_code&component_appid={}",
161            host(config),
162            app_id,
163            js_code,
164            component_app_id(config),
165        )
166    }
167
168    pub(super) fn have_open(config: &dyn WxOpenConfigStorage) -> String {
169        api(config, "/cgi-bin/open/have")
170    }
171    /// 创建开放平台帐号(对应 Java `WxOpenComponentService.CREATE_OPEN_URL`)。
172    pub(super) fn create_open(config: &dyn WxOpenConfigStorage) -> String {
173        api(config, "/cgi-bin/open/create")
174    }
175    /// 绑定开放平台帐号(对应 Java `BIND_OPEN_URL`)。
176    pub(super) fn bind_open(config: &dyn WxOpenConfigStorage) -> String {
177        api(config, "/cgi-bin/open/bind")
178    }
179    /// 解绑开放平台帐号(对应 Java `UNBIND_OPEN_URL`)。
180    pub(super) fn unbind_open(config: &dyn WxOpenConfigStorage) -> String {
181        api(config, "/cgi-bin/open/unbind")
182    }
183    /// 获取开放平台帐号(对应 Java `GET_OPEN_URL`)。
184    pub(super) fn get_open(config: &dyn WxOpenConfigStorage) -> String {
185        api(config, "/cgi-bin/open/get")
186    }
187    pub(super) fn get_template_draft_list(config: &dyn WxOpenConfigStorage) -> String {
188        api(config, "/wxa/gettemplatedraftlist")
189    }
190    pub(super) fn get_template_list(config: &dyn WxOpenConfigStorage) -> String {
191        api(config, "/wxa/gettemplatelist")
192    }
193    pub(super) fn add_to_template(config: &dyn WxOpenConfigStorage) -> String {
194        api(config, "/wxa/addtotemplate")
195    }
196    pub(super) fn delete_template(config: &dyn WxOpenConfigStorage) -> String {
197        api(config, "/wxa/deletetemplate")
198    }
199    pub(super) fn fast_register_weapp(config: &dyn WxOpenConfigStorage) -> String {
200        api(config, "/cgi-bin/component/fastregisterweapp?action=create")
201    }
202    pub(super) fn fast_register_weapp_search(config: &dyn WxOpenConfigStorage) -> String {
203        api(config, "/cgi-bin/component/fastregisterweapp?action=search")
204    }
205    pub(super) fn fast_register_personal_weapp(config: &dyn WxOpenConfigStorage) -> String {
206        api(
207            config,
208            "/wxa/component/fastregisterpersonalweapp?action=create",
209        )
210    }
211    pub(super) fn fast_register_personal_weapp_search(config: &dyn WxOpenConfigStorage) -> String {
212        api(
213            config,
214            "/wxa/component/fastregisterpersonalweapp?action=query",
215        )
216    }
217    pub(super) fn fast_register_beta_weapp(config: &dyn WxOpenConfigStorage) -> String {
218        api(config, "/wxa/component/fastregisterbetaweapp")
219    }
220    pub(super) fn register_shop(config: &dyn WxOpenConfigStorage) -> String {
221        api(config, "/product/register/register_shop")
222    }
223    pub(super) fn check_shop_audit_status(config: &dyn WxOpenConfigStorage) -> String {
224        api(config, "/product/register/check_audit_status")
225    }
226    pub(super) fn submit_merchant_info(config: &dyn WxOpenConfigStorage) -> String {
227        api(config, "/product/register/submit_merchantinfo")
228    }
229    pub(super) fn submit_basic_info(config: &dyn WxOpenConfigStorage) -> String {
230        api(config, "/product/register/submit_basicinfo")
231    }
232    /// 上传图片(对应 Java `UPLOAD_IMAGE_URL` 与 `WxOpenMinishopService.
233    /// UPLOAD_IMG_MINISHOP_FILE_URL`,同值)。
234    pub(super) fn upload_image(config: &dyn WxOpenConfigStorage) -> String {
235        api(config, "/product/img/upload")
236    }
237    pub(super) fn minishop_category_get(config: &dyn WxOpenConfigStorage) -> String {
238        api(config, "/product/category/get")
239    }
240    pub(super) fn minishop_brand_get(config: &dyn WxOpenConfigStorage) -> String {
241        api(config, "/product/brand/get")
242    }
243    pub(super) fn minishop_delivery_template_get(config: &dyn WxOpenConfigStorage) -> String {
244        api(config, "/product/delivery/get_freight_template")
245    }
246    pub(super) fn minishop_shopcategory_get(config: &dyn WxOpenConfigStorage) -> String {
247        api(config, "/product/store/get_shopcat")
248    }
249    pub(super) fn minishop_create_coupon(config: &dyn WxOpenConfigStorage) -> String {
250        api(config, "/product/coupon/create")
251    }
252    pub(super) fn minishop_get_coupon_list(config: &dyn WxOpenConfigStorage) -> String {
253        api(config, "/product/coupon/get_list")
254    }
255    pub(super) fn minishop_push_coupon(config: &dyn WxOpenConfigStorage) -> String {
256        api(config, "/product/coupon/push")
257    }
258    pub(super) fn minishop_update_coupon(config: &dyn WxOpenConfigStorage) -> String {
259        api(config, "/product/coupon/update")
260    }
261    pub(super) fn minishop_update_coupon_status(config: &dyn WxOpenConfigStorage) -> String {
262        api(config, "/product/coupon/update_status")
263    }
264    pub(super) fn minishop_get_delivery_company(config: &dyn WxOpenConfigStorage) -> String {
265        api(config, "/product/delivery/get_company_list")
266    }
267    pub(super) fn minishop_add_spu(config: &dyn WxOpenConfigStorage) -> String {
268        api(config, "/product/spu/add")
269    }
270    pub(super) fn minishop_del_spu(config: &dyn WxOpenConfigStorage) -> String {
271        api(config, "/product/spu/del")
272    }
273    pub(super) fn minishop_update_spu(config: &dyn WxOpenConfigStorage) -> String {
274        api(config, "/product/spu/update")
275    }
276    pub(super) fn minishop_listing_spu(config: &dyn WxOpenConfigStorage) -> String {
277        api(config, "/product/spu/listing")
278    }
279    pub(super) fn minishop_delisting_spu(config: &dyn WxOpenConfigStorage) -> String {
280        api(config, "/product/spu/delisting")
281    }
282    pub(super) fn minishop_add_sku(config: &dyn WxOpenConfigStorage) -> String {
283        api(config, "/product/sku/add")
284    }
285    pub(super) fn minishop_batch_add_sku(config: &dyn WxOpenConfigStorage) -> String {
286        api(config, "/product/sku/batch_add")
287    }
288    pub(super) fn minishop_del_sku(config: &dyn WxOpenConfigStorage) -> String {
289        api(config, "/product/sku/del")
290    }
291    pub(super) fn minishop_update_sku(config: &dyn WxOpenConfigStorage) -> String {
292        api(config, "/product/sku/update")
293    }
294    pub(super) fn minishop_update_sku_price(config: &dyn WxOpenConfigStorage) -> String {
295        api(config, "/product/sku/update_price")
296    }
297    pub(super) fn minishop_update_sku_stock(config: &dyn WxOpenConfigStorage) -> String {
298        api(config, "/product/stock/update")
299    }
300    /// 注意:Java `API_MINISHOP_ADD_LIMIT_DISCOUNT_URL` 后直接拼接
301    /// `"access_token="`(无 "?"),镜像该线格式。
302    pub(super) fn minishop_add_limit_discount(config: &dyn WxOpenConfigStorage) -> String {
303        api(config, "/product/limiteddiscount/add/")
304    }
305    pub(super) fn minishop_get_limit_discount(config: &dyn WxOpenConfigStorage) -> String {
306        api(config, "/product/limiteddiscount/get_list/")
307    }
308    pub(super) fn minishop_update_limit_discount_status(
309        config: &dyn WxOpenConfigStorage,
310    ) -> String {
311        api(config, "/product/limiteddiscount/update_status/")
312    }
313    pub(super) fn batch_get_env_id(config: &dyn WxOpenConfigStorage) -> String {
314        api(config, "/componenttcb/batchgetenvid")
315    }
316    pub(super) fn describe_envs(config: &dyn WxOpenConfigStorage) -> String {
317        api(config, "/componenttcb/describeenvs")
318    }
319    pub(super) fn modify_env(config: &dyn WxOpenConfigStorage) -> String {
320        api(config, "/tcb/modifyenv")
321    }
322    pub(super) fn batch_share_env(config: &dyn WxOpenConfigStorage) -> String {
323        api(config, "/componenttcb/batchshareenv")
324    }
325    pub(super) fn clear_quota(config: &dyn WxOpenConfigStorage) -> String {
326        api(config, "/cgi-bin/component/clear_quota/v2")
327    }
328    pub(super) fn modify_wxa_server_domain(config: &dyn WxOpenConfigStorage) -> String {
329        api(config, "/cgi-bin/component/modify_wxa_server_domain")
330    }
331    pub(super) fn get_domain_confirm_file(config: &dyn WxOpenConfigStorage) -> String {
332        api(config, "/cgi-bin/component/get_domain_confirmfile")
333    }
334    pub(super) fn modify_wxa_jump_domain(config: &dyn WxOpenConfigStorage) -> String {
335        api(config, "/cgi-bin/component/modify_wxa_jump_domain")
336    }
337    pub(super) fn apply_set_order_path_info(config: &dyn WxOpenConfigStorage) -> String {
338        api(config, "/wxa/security/applysetorderpathinfo")
339    }
340
341    fn host(config: &dyn WxOpenConfigStorage) -> String {
342        config
343            .wx_open_host_config()
344            .map(|h| h.api_host)
345            .unwrap_or_else(|| API_DEFAULT_HOST_URL.to_string())
346    }
347
348    fn component_app_id(config: &dyn WxOpenConfigStorage) -> String {
349        config.component_app_id().unwrap_or_default()
350    }
351}
352
353// ---------------------------------------------------------------------------
354// minishop 入参 JSON 拼装(逐键镜像 Java bean 的 `toJsonObject()` snake_case
355// 线格式;Rust bean serde 为 camelCase,不可直接序列化)
356// ---------------------------------------------------------------------------
357mod minishop_json {
358    use serde_json::{Value, json};
359
360    use crate::bean::{
361        LimitDiscountGoods, LimitDiscountSku, MinishopAddressInfo, MinishopBusiLicense,
362        MinishopIdcardInfo, MinishopNameInfo, MinishopOrganizationCodeInfo, MinishopPicFile,
363        MinishopReturnInfo, MinishopShopCat, MinishopSuperAdministratorInfo, WxMinishopCoupon,
364        WxMinishopCouponDiscountCondition, WxMinishopCouponDiscountInfo, WxMinishopCouponExtInfo,
365        WxMinishopCouponPromoteInfo, WxMinishopCouponReceiveInfo, WxMinishopCouponValidInfo,
366        WxMinishopGoodsSkuAttr, WxMinishopSku, WxMinishopSpu,
367    };
368
369    /// 对应 Java `MinishopPicFile.toJsonObject()`。
370    pub(super) fn pic_file(p: &MinishopPicFile) -> Value {
371        json!({ "media_id": p.media_id, "pay_media_id": p.pay_media_id })
372    }
373
374    /// 对应 Java `MinishopBusiLicense.toJsonObject()`。
375    pub(super) fn busi_license(b: &MinishopBusiLicense) -> Value {
376        let mut v = json!({
377            "license_type": b.license_type,
378            "pic_file": pic_file(&b.pic_file),
379            "registration_num": b.registration_num,
380            "merchant_name": b.merchant_name,
381            "legal_representative": b.legal_representative,
382            "start_date": b.start_date,
383            "end_date": b.end_date,
384        });
385        // Java `if (registeredAddrs != null)`;Rust String 非空判断镜像
386        if !b.registered_addrs.is_empty() {
387            v["registered_addrs"] = json!(b.registered_addrs);
388        }
389        v
390    }
391
392    /// 对应 Java `MinishopOrganizationCodeInfo.toJsonObject()`。
393    pub(super) fn organization_code_info(o: &MinishopOrganizationCodeInfo) -> Value {
394        json!({
395            "pic_file": pic_file(&o.pic_file),
396            "organization_code": o.organization_code,
397            "start_date": o.start_date,
398            "end_date": o.end_date,
399        })
400    }
401
402    /// 对应 Java `MinishopIdcardInfo.toJsonObject()`。
403    pub(super) fn idcard_info(i: &MinishopIdcardInfo) -> Value {
404        json!({
405            "portrait_pic_file": pic_file(&i.portrait_pic_file),
406            "nation_pic_file": pic_file(&i.nation_pic_file),
407            "id_card_name": i.id_card_name,
408            "id_card_number": i.id_card_number,
409            "start_date": i.start_date,
410            "end_date": i.end_date,
411        })
412    }
413
414    /// 对应 Java `MinishopSuperAdministratorInfo.toJsonObject()`。
415    pub(super) fn super_administrator_info(s: &MinishopSuperAdministratorInfo) -> Value {
416        json!({
417            "type": s.r#type,
418            "name": s.name,
419            "id_card_number": s.id_card_number,
420            "phone": s.phone,
421            "mail": s.mail,
422        })
423    }
424
425    /// 对应 Java `MinishopNameInfo.toJsonObject()`。
426    pub(super) fn name_info(n: &MinishopNameInfo) -> Value {
427        json!({
428            "nickname": n.nick_name,
429            "abbr": n.abbr,
430            "introduction": n.introduction,
431        })
432    }
433
434    /// 对应 Java `MinishopAddressInfo.toJsonObject()`。
435    pub(super) fn address_info(a: &MinishopAddressInfo) -> Value {
436        json!({
437            "user_name": a.user_name,
438            "postal_code": a.postal_code,
439            "province_name": a.province,
440            "city_name": a.city_name,
441            "county_name": a.county_name,
442            "detail_info": a.detail_info,
443            "national_code": a.national_code,
444            "tel_number": a.tel_number,
445        })
446    }
447
448    /// 对应 Java `MinishopReturnInfo.toJsonObject()`。
449    pub(super) fn return_info(r: &MinishopReturnInfo) -> Value {
450        json!({
451            "address_info": address_info(&r.address_info),
452            "mail": r.email,
453            "company_address": address_info(&r.company_address),
454        })
455    }
456
457    /// 对应 Java `WxMinishopGoodsSkuAttr.toJsonObject()`。
458    pub(super) fn sku_attr(a: &WxMinishopGoodsSkuAttr) -> Value {
459        json!({ "attr_key": a.attr_key, "attr_value": a.attr_value })
460    }
461
462    /// 对应 Java `WxMinishopShopCat`(spu 中 cats 元素:cat_id/level)。
463    pub(super) fn shop_cat(c: &MinishopShopCat) -> Value {
464        json!({ "cat_id": c.shop_cat_id, "level": c.cat_level })
465    }
466
467    /// 对应 Java `WxMinishopSku.toJsonObject()`。
468    pub(super) fn sku(s: &WxMinishopSku) -> Value {
469        let attrs: Vec<Value> = s.sku_attrs.iter().map(sku_attr).collect();
470        json!({
471            "product_id": s.product_id,
472            "out_product_id": s.out_product_id,
473            "out_sku_id": s.out_sku_id,
474            "thumb_img": s.thumb_img,
475            "sale_price": s.sale_price,
476            "market_price": s.market_price,
477            "stock_num": s.stock_num,
478            "sku_code": s.sku_code,
479            "barcode": s.bar_code,
480            "sku_attrs": attrs,
481        })
482    }
483
484    /// 对应 Java `WxMinishopSpu.toJsonObject()`。
485    pub(super) fn spu(spu: &WxMinishopSpu) -> Value {
486        let cats: Vec<Value> = spu.shop_cats.iter().map(shop_cat).collect();
487        let attrs: Vec<Value> = spu.attrs.iter().map(sku_attr).collect();
488        let skus: Vec<Value> = spu.skus.iter().map(sku).collect();
489        json!({
490            "out_product_id": spu.out_product_id,
491            "title": spu.title,
492            "sub_title": spu.sub_title,
493            "head_img": spu.head_imgs,
494            "desc_info": { "imgs": spu.desc_info_imgs },
495            "brand_id": spu.brand_id,
496            "cats": cats,
497            "attrs": attrs,
498            "model": spu.model,
499            "express_info": { "template_id": spu.express_template_id },
500            "skus": skus,
501        })
502    }
503
504    /// 对应 Java `WxMinishopCouponDiscountCondition.toJsonObject()`。
505    pub(super) fn coupon_discount_condition(c: &WxMinishopCouponDiscountCondition) -> Value {
506        json!({
507            "product_cnt": c.product_cnt,
508            "product_ids": c.product_ids,
509            "product_price": c.product_price,
510        })
511    }
512
513    /// 对应 Java `WxMinishopCouponDiscountInfo.toJsonObject()`。
514    pub(super) fn coupon_discount_info(d: &WxMinishopCouponDiscountInfo) -> Value {
515        json!({
516            "discount_condition": coupon_discount_condition(&d.discount_condition),
517            "discount_fee": d.discount_fee,
518            "discount_num": d.discount_num,
519        })
520    }
521
522    /// 对应 Java `WxMinishopCouponExtInfo.toJsonObject()`。
523    pub(super) fn coupon_ext_info(e: &WxMinishopCouponExtInfo) -> Value {
524        json!({
525            "jump_product_id": e.jump_product_id,
526            "notes": e.notes,
527            "valid_time": e.valid_time,
528            "invalid_time": e.invalid_time,
529        })
530    }
531
532    /// 对应 Java `WxMinishopCouponPromoteInfo.toJsonObject()`。
533    pub(super) fn coupon_promote_info(p: &WxMinishopCouponPromoteInfo) -> Value {
534        json!({
535            "customize_channel": p.customize_channel,
536            "promote_type": p.promotion_type,
537        })
538    }
539
540    /// 对应 Java `WxMinishopCouponReceiveInfo.toJsonObject()`。
541    pub(super) fn coupon_receive_info(r: &WxMinishopCouponReceiveInfo) -> Value {
542        json!({
543            "start_time": r.start_time,
544            "end_time": r.end_time,
545            "limit_num_one_person": r.limit_num_one_person,
546            "total_num": r.total_num,
547        })
548    }
549
550    /// 对应 Java `WxMinishopCouponValidInfo.toJsonObject()`。
551    pub(super) fn coupon_valid_info(v: &WxMinishopCouponValidInfo) -> Value {
552        json!({
553            "start_time": v.start_time,
554            "end_time": v.end_time,
555            "valid_day_num": v.valid_day_num,
556            "valid_type": v.valid_type,
557        })
558    }
559
560    /// 对应 Java `WxMinishopCoupon.toJsonObject()`。
561    pub(super) fn coupon(c: &WxMinishopCoupon) -> Value {
562        let mut v = json!({
563            "type": c.r#type,
564            "name": c.name,
565        });
566        // Java `if (couponId != null)`:以 -1 表达未设置(ADAPTED,见 bean)
567        if c.coupon_id >= 0 {
568            v["coupon_id"] = json!(c.coupon_id);
569        }
570        if c.status >= 0 {
571            v["status"] = json!(c.status);
572        }
573        v["discount_info"] = coupon_discount_info(&c.discount_info);
574        v["ext_info"] = coupon_ext_info(&c.ext_info);
575        v["promote_info"] = coupon_promote_info(&c.promote_info);
576        v["receive_info"] = coupon_receive_info(&c.receive_info);
577        v["valid_info"] = coupon_valid_info(&c.valid_info);
578        v
579    }
580
581    /// 对应 Java `LimitDiscountSku.toJsonObject()`(sale_price 以分为单位:
582    /// Java `salePrice.multiply(100).longValue()`;Rust 侧 bean 以 String
583    /// 存金额,解析为 f64 后乘 100,ADAPTED)。
584    pub(super) fn limit_discount_sku(s: &LimitDiscountSku) -> Value {
585        let sale_price_cents = s
586            .sale_price
587            .parse::<f64>()
588            .map(|p| (p * 100.0).round() as i64)
589            .unwrap_or(0);
590        json!({
591            "sku_id": s.sku_id,
592            "sale_price": sale_price_cents,
593            "sale_stock": s.sale_stock,
594        })
595    }
596
597    /// 对应 Java `LimitDiscountGoods.toJsonObject()`。
598    pub(super) fn limit_discount_goods(g: &LimitDiscountGoods) -> Value {
599        let mut v = json!({
600            "product_id": g.product_id,
601            "start_time": g.start_time,
602            "end_time": g.end_time,
603        });
604        if g.task_id != 0 {
605            v["task_id"] = json!(g.task_id);
606        }
607        if g.status != 0 {
608            v["status"] = json!(g.status);
609        }
610        let skus: Vec<Value> = g
611            .limit_discount_sku_list
612            .iter()
613            .map(limit_discount_sku)
614            .collect();
615        v["limited_discount_sku_list"] = json!(skus);
616        v
617    }
618}
619
620/// 组件服务实现。
621pub struct WxOpenComponentServiceImpl {
622    wx_open_service: Weak<dyn WxOpenService>,
623    /// 代 mp 服务缓存(对应 Java 静态 `WX_OPEN_MP_SERVICE_MAP`
624    /// ConcurrentHashMap + synchronized 双检锁;Rust 以实例级
625    /// `Mutex<HashMap>` 表达,值经具体类型上转为
626    /// `Arc<dyn Any + Send + Sync>`,调用方 downcast 下转)。
627    mp_services: Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>,
628    /// 代 ma 服务缓存(对应 Java 静态 `WX_OPEN_MA_SERVICE_MAP`)。
629    ma_services: Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>,
630    /// 代 fast_ma 服务缓存(对应 Java 静态 `WX_OPEN_FAST_MA_SERVICE_MAP`;
631    /// Java fast_ma 为独立类但语义与 ma 桥接相同,Rust 统一以
632    /// [`WxOpenMaService`] 承载,ADAPTED)。
633    fast_ma_services: Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>,
634    /// 小商店服务缓存(对应 Java 静态 `WX_OPEN_MINISHOP_SERVICE_MAP`,
635    /// 装配 [`WxOpenMinishopServiceImpl`])。
636    minishop_services: Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>,
637}
638
639impl WxOpenComponentServiceImpl {
640    /// 构建组件服务(对应 Java `new WxOpenComponentServiceImpl(this)`)。
641    ///
642    /// # 参数
643    /// - `wx_open_service`:门面服务弱引用(Rust 以弱引用打破循环引用)
644    pub fn new(wx_open_service: Weak<dyn WxOpenService>) -> Self {
645        Self {
646            wx_open_service,
647            mp_services: Mutex::new(HashMap::new()),
648            ma_services: Mutex::new(HashMap::new()),
649            fast_ma_services: Mutex::new(HashMap::new()),
650            minishop_services: Mutex::new(HashMap::new()),
651        }
652    }
653
654    /// 升级门面服务引用;门面已释放时返回业务错误。
655    fn svc(&self) -> Result<Arc<dyn WxOpenService>, WxErrorException> {
656        self.wx_open_service
657            .upgrade()
658            .ok_or_else(|| WxErrorException::from_code(-99, "门面服务已被释放"))
659    }
660
661    /// 裸 POST(对应 Java `getWxOpenService().post(url, postData)`:URL 已
662    /// 含 access_token 或无需 token,不做 component_access_token 注入)。
663    async fn bare_post(&self, uri: &str, post_data: &str) -> Result<String, WxErrorException> {
664        let svc = self.svc()?;
665        let executor = SimplePostRequestExecutor::new(svc.http_client().clone());
666        executor
667            .execute(uri, post_data.to_string(), WxType::Open)
668            .await
669    }
670
671    /// 微信开放平台帐号管理统一请求入口(对应 Java 私有方法
672    /// `openAccountServicePost(String appId, String appIdType, String
673    /// requestUrl, JsonObject param)`)。
674    ///
675    /// 镜像 Java switch:`mp` → 代 mp 服务([`WxOpenMpService`])的 post
676    /// (mp 执行引擎注入 authorizer access_token);`mini` → 代 ma 服务
677    /// ([`WxOpenMaService`])的 post;其余类型抛「appIdType类型异常」。
678    ///
679    /// ADAPTED:Java 直接抛 `new WxErrorException("appIdType类型异常")`,
680    /// Rust 统一以 `Err(-99)` 表达;`Arc<dyn Any>` 下转失败(缓存值类型
681    /// 不匹配)同样以 `Err(-99)` 表达(Java 强类型不可能出现)。
682    async fn open_account_service_post(
683        &self,
684        app_id: &str,
685        app_id_type: &str,
686        request_url: &str,
687        param: &serde_json::Value,
688    ) -> Result<String, WxErrorException> {
689        match app_id_type {
690            app_id_type::MP_TYPE => {
691                let any = self.get_wx_mp_service_by_appid(app_id).ok_or_else(|| {
692                    WxErrorException::from_code(-99, "getWxMpServiceByAppid 返回 None")
693                })?;
694                let mp = any.downcast::<WxOpenMpService>().map_err(|_| {
695                    WxErrorException::from_code(-99, "代 mp 服务 downcast 失败(缓存类型不匹配)")
696                })?;
697                let mp: Arc<dyn WxMpService> = mp;
698                mp.post(request_url, &param.to_string()).await
699            }
700            app_id_type::MINI_TYPE => {
701                let any = self.get_wx_ma_service_by_appid(app_id).ok_or_else(|| {
702                    WxErrorException::from_code(-99, "getWxMaServiceByAppid 返回 None")
703                })?;
704                let ma = any.downcast::<WxOpenMaService>().map_err(|_| {
705                    WxErrorException::from_code(-99, "代 ma 服务 downcast 失败(缓存类型不匹配)")
706                })?;
707                let ma: Arc<dyn WxMaService> = ma;
708                ma.post(request_url, &param.to_string()).await
709            }
710            _ => Err(WxErrorException::from_code(-99, "appIdType类型异常")),
711        }
712    }
713
714    /// 创建预授权链接(对应 Java 私有方法
715    /// `createPreAuthUrl(String, String, String, boolean)`)。
716    ///
717    /// 流程:POST `api_create_preauthcode` 取 `pre_auth_code` → 按
718    /// `COMPONENT_LOGIN_PAGE_URL`/`COMPONENT_MOBILE_LOGIN_PAGE_URL` 格式化
719    /// (redirect_uri 经 JS `encodeURIComponent` 语义编码)→ auth_type/
720    /// biz_appid 非空时替换 `xxx` 占位(Java 对空值替换为空串,此处以
721    /// 只替换非空值表达,其余占位符保留由调用方处理——Wave 0 冻结语义)。
722    async fn create_pre_auth_url(
723        &self,
724        redirect_uri: &str,
725        auth_type: Option<&str>,
726        biz_appid: Option<&str>,
727        is_mobile: bool,
728    ) -> Result<String, WxErrorException> {
729        let svc = self.svc()?;
730        let config = svc.wx_open_config_storage();
731        let component_app_id = config.component_app_id().unwrap_or_default().to_string();
732        let pre_auth_code = self.get_pre_auth_code().await?;
733
734        // Java `URIUtil.encodeURIComponent(redirectUri)`:JS encodeURIComponent 语义
735        let encoded_redirect = utf8_percent_encode(redirect_uri, NON_ALPHANUMERIC).to_string();
736        let mut pre_auth_url_str = if is_mobile {
737            component_mobile_login_page_url(&component_app_id, &pre_auth_code, &encoded_redirect)
738        } else {
739            component_login_page_url(&component_app_id, &pre_auth_code, &encoded_redirect)
740        };
741        // Java `StringUtils.isNotEmpty(authType)` 才替换,否则移除占位符
742        if let Some(auth_type) = auth_type {
743            if !auth_type.is_empty() {
744                pre_auth_url_str =
745                    pre_auth_url_str.replace("&auth_type=xxx", &format!("&auth_type={auth_type}"));
746            } else {
747                pre_auth_url_str = pre_auth_url_str.replace("&auth_type=xxx", "");
748            }
749        } else {
750            pre_auth_url_str = pre_auth_url_str.replace("&auth_type=xxx", "");
751        }
752        if let Some(biz_appid) = biz_appid {
753            if !biz_appid.is_empty() {
754                pre_auth_url_str =
755                    pre_auth_url_str.replace("&biz_appid=xxx", &format!("&biz_appid={biz_appid}"));
756            } else {
757                pre_auth_url_str = pre_auth_url_str.replace("&biz_appid=xxx", "");
758            }
759        } else {
760            pre_auth_url_str = pre_auth_url_str.replace("&biz_appid=xxx", "");
761        }
762        Ok(pre_auth_url_str)
763    }
764
765    /// 取 JSON 值辅助(Java `getAsInt` 语义:缺失/非数字时默认值)。
766    fn json_i64(v: &serde_json::Value, key: &str) -> i64 {
767        v.get(key).and_then(|x| x.as_i64()).unwrap_or(0)
768    }
769
770    fn json_str(v: &serde_json::Value, key: &str) -> String {
771        v.get(key)
772            .and_then(|x| x.as_str())
773            .unwrap_or_default()
774            .to_string()
775    }
776
777    /// 归一化 errcode 为字符串(对应 Java Gson 宽松类型转换语义)。
778    ///
779    /// 微信接口 `errcode` 为数字(如 `0`),Java 各 bean 的 `String errcode`
780    /// 经 Gson 自动转字符串(`"0"`);Rust serde 严格,数字无法反序列化
781    /// 进 String 字段(生成的 bean 与 Java 字段类型一致,冻结),故在
782    /// bean 解析前将数字 errcode 归一化为字符串——严格镜像 Java 原语义
783    /// (ADAPTED,见 open_account 实现注释)。
784    fn normalize_errcode(json: &str) -> Result<String, WxErrorException> {
785        let mut value: serde_json::Value =
786            serde_json::from_str(json).map_err(|e| WxErrorException::Serde(e.to_string()))?;
787        if let Some(errcode) = value.get("errcode") {
788            if errcode.is_number() {
789                value["errcode"] = serde_json::Value::String(errcode.to_string());
790            }
791        }
792        serde_json::to_string(&value).map_err(|e| WxErrorException::Serde(e.to_string()))
793    }
794}
795
796#[async_trait]
797impl WxOpenComponentService for WxOpenComponentServiceImpl {
798    fn wx_open_service(&self) -> Option<Arc<dyn WxOpenService>> {
799        self.wx_open_service.upgrade()
800    }
801
802    /// 获取指定 appid 的开放平台公众号服务(对应 Java
803    /// `getWxMpServiceByAppid(String appid)`,双检锁缓存按 appid 装配
804    /// `WxOpenMpServiceImpl`)。
805    ///
806    /// 镜像 Java:先无锁读缓存,未命中再持锁二次检查后装配
807    /// [`WxOpenMpService`](构造时快照门面配置存储与 HTTP 客户端)。
808    /// 返回值以 `Arc<dyn Any + Send + Sync>` 承载(trait 签名冻结),
809    /// 调用方经 [`crate::api::r#impl::downcast_mp_service`] 下转为
810    /// `Arc<dyn WxMpService>`。
811    fn get_wx_mp_service_by_appid(&self, app_id: &str) -> Option<Arc<dyn Any + Send + Sync>> {
812        // Java `WX_OPEN_MP_SERVICE_MAP.get(appId)`(无锁首查)
813        if let Some(svc) = self.mp_services.lock().unwrap().get(app_id) {
814            return Some(svc.clone());
815        }
816        // Java `synchronized (WX_OPEN_MP_SERVICE_MAP)` 二次检查
817        let mut map = self.mp_services.lock().unwrap();
818        if let Some(svc) = map.get(app_id) {
819            return Some(svc.clone());
820        }
821        let open_svc = self.svc().ok()?;
822        // 先经具体类型上转(`Arc<WxOpenMpService>` → `Arc<dyn Any>`),
823        // 保证调用方按具体类型 downcast 成功;new_arc 装配代公众号 oauth2
824        // 服务(对应 Java 构造器 setOAuth2Service)
825        let svc: Arc<dyn Any + Send + Sync> =
826            WxOpenMpService::new_arc(open_svc, app_id.to_string());
827        map.insert(app_id.to_string(), svc.clone());
828        Some(svc)
829    }
830
831    /// 获取指定 appid 的开放平台小程序服务(对应 Java
832    /// `getWxMaServiceByAppid(String appid)`,双检锁缓存装配
833    /// `WxOpenMaServiceImpl`)。
834    ///
835    /// 返回值经 [`crate::api::r#impl::downcast_ma_service`] 下转为
836    /// `Arc<dyn WxMaService>`。
837    fn get_wx_ma_service_by_appid(&self, app_id: &str) -> Option<Arc<dyn Any + Send + Sync>> {
838        if let Some(svc) = self.ma_services.lock().unwrap().get(app_id) {
839            return Some(svc.clone());
840        }
841        let mut map = self.ma_services.lock().unwrap();
842        if let Some(svc) = map.get(app_id) {
843            return Some(svc.clone());
844        }
845        let open_svc = self.svc().ok()?;
846        let svc: Arc<dyn Any + Send + Sync> =
847            Arc::new(WxOpenMaService::new(open_svc, app_id.to_string()));
848        map.insert(app_id.to_string(), svc.clone());
849        Some(svc)
850    }
851
852    /// 获取指定 appid 的快速创建的小程序服务(对应 Java
853    /// `getWxFastMaServiceByAppid(String appid)`)。
854    ///
855    /// Java `@Deprecated`(2021-06-23:请使用 `WxOpenMaService`)。
856    /// Java 以独立类 `WxOpenFastMaServiceImpl`(extends WxMaServiceImpl,
857    /// getAccessToken 同委托 authorizer token)装配;Rust 语义等价,
858    /// 统一以 [`WxOpenMaService`] 承载(独立缓存桶镜像 Java 独立 map,
859    /// ADAPTED),返回值经 [`crate::api::r#impl::downcast_ma_service`]
860    /// 下转为 `Arc<dyn WxMaService>`。
861    fn get_wx_fast_ma_service_by_appid(&self, app_id: &str) -> Option<Arc<dyn Any + Send + Sync>> {
862        if let Some(svc) = self.fast_ma_services.lock().unwrap().get(app_id) {
863            return Some(svc.clone());
864        }
865        let mut map = self.fast_ma_services.lock().unwrap();
866        if let Some(svc) = map.get(app_id) {
867            return Some(svc.clone());
868        }
869        let open_svc = self.svc().ok()?;
870        let svc: Arc<dyn Any + Send + Sync> =
871            Arc::new(WxOpenMaService::new(open_svc, app_id.to_string()));
872        map.insert(app_id.to_string(), svc.clone());
873        Some(svc)
874    }
875
876    /// 获取指定 appid 的小商店服务(对应 Java
877    /// `getWxMinishopServiceByAppid(String appid)`,双检锁缓存装配
878    /// `WxOpenMinishopServiceImpl`)。
879    ///
880    /// Wave 5 接线(此前恒 None):镜像 Java 静态
881    /// `WX_OPEN_MINISHOP_SERVICE_MAP` + synchronized 双检锁;Rust 以
882    /// 实例级 `Mutex<HashMap>` 表达,装配 [`WxOpenMinishopServiceImpl`]
883    /// (构造入参 appId + 门面弱引用,Java 传 `this` 组件服务 + appId +
884    /// wxMaConfig,语义一致,ADAPTED)。返回值以
885    /// `Arc<dyn Any + Send + Sync>` 承载,调用方按具体类型
886    /// `WxOpenMinishopServiceImpl` downcast 下转。
887    fn get_wx_minishop_service_by_appid(&self, app_id: &str) -> Option<Arc<dyn Any + Send + Sync>> {
888        if let Some(svc) = self.minishop_services.lock().unwrap().get(app_id) {
889            return Some(svc.clone());
890        }
891        let mut map = self.minishop_services.lock().unwrap();
892        if let Some(svc) = map.get(app_id) {
893            return Some(svc.clone());
894        }
895        let open_svc = self.svc().ok()?;
896        let svc: Arc<dyn Any + Send + Sync> =
897            Arc::new(WxOpenMinishopServiceImpl::new(open_svc, app_id.to_string()));
898        map.insert(app_id.to_string(), svc.clone());
899        Some(svc)
900    }
901
902    async fn start_push_ticket(&self) -> Result<(), WxErrorException> {
903        let svc = self.svc()?;
904        let config = svc.wx_open_config_storage();
905        // Java `jsonObject`:component_appid + component_secret
906        let body = serde_json::json!({
907            "component_appid": config.component_app_id().unwrap_or_default(),
908            "component_secret": config.component_app_secret().unwrap_or_default(),
909        });
910        self.post(
911            &api_start_push_ticket_url(config.as_ref()),
912            &body.to_string(),
913        )
914        .await?;
915        Ok(())
916    }
917
918    async fn get_component_access_token(
919        &self,
920        force_refresh: bool,
921    ) -> Result<String, WxErrorException> {
922        let svc = self.svc()?;
923        base_wx_open_service_impl::get_component_access_token_with_lock(svc.as_ref(), force_refresh)
924            .await
925    }
926
927    async fn post(&self, uri: &str, post_data: &str) -> Result<String, WxErrorException> {
928        let svc = self.svc()?;
929        let executor = SimplePostRequestExecutor::new(svc.http_client().clone());
930        base_wx_open_service_impl::execute_with_retry(
931            svc.as_ref(),
932            &executor,
933            uri,
934            post_data.to_string(),
935            ACCESS_TOKEN_KEY_COMPONENT,
936        )
937        .await
938    }
939
940    async fn post_with_key(
941        &self,
942        uri: &str,
943        post_data: &str,
944        access_token_key: &str,
945    ) -> Result<String, WxErrorException> {
946        let svc = self.svc()?;
947        let executor = SimplePostRequestExecutor::new(svc.http_client().clone());
948        base_wx_open_service_impl::execute_with_retry(
949            svc.as_ref(),
950            &executor,
951            uri,
952            post_data.to_string(),
953            access_token_key,
954        )
955        .await
956    }
957
958    async fn post_with_token(
959        &self,
960        uri: &str,
961        post_data: &str,
962        access_token_key: &str,
963        access_token: &str,
964    ) -> Result<String, WxErrorException> {
965        // Java:显式 token 拼进 uri,经门面裸 post 发送(无自动刷新);
966        // 执行器对 errcode!=0 已抛错,Java 中 `errorCode()==0` 分支
967        // (返回 errorMsg)理论不可达,直接上抛(与 base 引擎同一约定)
968        let uri_with_token = if uri.contains('?') {
969            format!("{uri}&{access_token_key}={access_token}")
970        } else {
971            format!("{uri}?{access_token_key}={access_token}")
972        };
973        self.bare_post(&uri_with_token, post_data).await
974    }
975
976    async fn get(&self, uri: &str) -> Result<String, WxErrorException> {
977        let svc = self.svc()?;
978        let executor = SimpleGetRequestExecutor::new(svc.http_client().clone());
979        base_wx_open_service_impl::execute_with_retry(
980            svc.as_ref(),
981            &executor,
982            uri,
983            String::new(),
984            ACCESS_TOKEN_KEY_COMPONENT,
985        )
986        .await
987    }
988
989    async fn get_with_key(
990        &self,
991        uri: &str,
992        access_token_key: &str,
993    ) -> Result<String, WxErrorException> {
994        let svc = self.svc()?;
995        let executor = SimpleGetRequestExecutor::new(svc.http_client().clone());
996        base_wx_open_service_impl::execute_with_retry(
997            svc.as_ref(),
998            &executor,
999            uri,
1000            String::new(),
1001            access_token_key,
1002        )
1003        .await
1004    }
1005
1006    async fn get_pre_auth_code(&self) -> Result<String, WxErrorException> {
1007        let svc = self.svc()?;
1008        let config = svc.wx_open_config_storage();
1009        // Java `jsonObject.addProperty("component_appid", ...)`
1010        let body = serde_json::json!({
1011            "component_appid": config.component_app_id().unwrap_or_default(),
1012        });
1013        let response = self
1014            .post(
1015                &api_create_preauthcode_url(config.as_ref()),
1016                &body.to_string(),
1017            )
1018            .await?;
1019        let json: serde_json::Value =
1020            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1021        json.get("pre_auth_code")
1022            .and_then(|v| v.as_str())
1023            .map(str::to_string)
1024            .ok_or_else(|| WxErrorException::from_code(-99, "pre_auth_code 字段缺失"))
1025    }
1026
1027    async fn get_pre_auth_url(&self, redirect_uri: &str) -> Result<String, WxErrorException> {
1028        self.create_pre_auth_url(redirect_uri, None, None, false)
1029            .await
1030    }
1031
1032    async fn get_pre_auth_url_with(
1033        &self,
1034        redirect_uri: &str,
1035        auth_type: Option<&str>,
1036        biz_appid: Option<&str>,
1037    ) -> Result<String, WxErrorException> {
1038        self.create_pre_auth_url(redirect_uri, auth_type, biz_appid, false)
1039            .await
1040    }
1041
1042    async fn get_mobile_pre_auth_url(
1043        &self,
1044        redirect_uri: &str,
1045    ) -> Result<String, WxErrorException> {
1046        self.create_pre_auth_url(redirect_uri, None, None, true)
1047            .await
1048    }
1049
1050    async fn get_mobile_pre_auth_url_with(
1051        &self,
1052        redirect_uri: &str,
1053        auth_type: Option<&str>,
1054        biz_appid: Option<&str>,
1055    ) -> Result<String, WxErrorException> {
1056        self.create_pre_auth_url(redirect_uri, auth_type, biz_appid, true)
1057            .await
1058    }
1059
1060    async fn route(&self, message: &WxOpenXmlMessage) -> Result<String, WxErrorException> {
1061        // Java 先判 null 抛 NPE;Rust 入参非空类型,跳过
1062        let info_type = message.info_type.as_deref().unwrap_or_default();
1063        // verify_ticket:存储链(component_verify_ticket → config)
1064        if info_type.eq_ignore_ascii_case("component_verify_ticket") {
1065            let config = self.svc()?.wx_open_config_storage();
1066            if let Some(ticket) = &message.component_verify_ticket {
1067                config.set_component_verify_ticket(ticket);
1068            }
1069            return Ok("success".to_string());
1070        }
1071        // 新增、更新授权:授权码换授权信息(回写 authorizer token)
1072        if info_type.eq_ignore_ascii_case("authorized")
1073            || info_type.eq_ignore_ascii_case("updateauthorized")
1074        {
1075            let code = message.authorization_code.as_deref().unwrap_or_default();
1076            let query_auth = self.get_query_auth(code).await?;
1077            if query_auth
1078                .authorization_info
1079                .as_ref()
1080                .and_then(|i| i.authorizer_appid.as_ref())
1081                .is_none()
1082            {
1083                // 对应 Java `throw new NullPointerException("getQueryAuth")`
1084                return Err(WxErrorException::from_code(-99, "getQueryAuth"));
1085            }
1086            return Ok("success".to_string());
1087        }
1088        // 快速创建小程序
1089        if info_type.eq_ignore_ascii_case("notify_third_fasteregister") && message.status == Some(0)
1090        {
1091            let code = message.auth_code.as_deref().unwrap_or_default();
1092            let query_auth = self.get_query_auth(code).await?;
1093            if query_auth
1094                .authorization_info
1095                .as_ref()
1096                .and_then(|i| i.authorizer_appid.as_ref())
1097                .is_none()
1098            {
1099                return Err(WxErrorException::from_code(-99, "getQueryAuth"));
1100            }
1101            return Ok("success".to_string());
1102        }
1103        Ok(String::new())
1104    }
1105
1106    async fn get_query_auth(
1107        &self,
1108        authorization_code: &str,
1109    ) -> Result<WxOpenQueryAuthResult, WxErrorException> {
1110        let svc = self.svc()?;
1111        let config = svc.wx_open_config_storage();
1112        let body = serde_json::json!({
1113            "component_appid": config.component_app_id().unwrap_or_default(),
1114            "authorization_code": authorization_code,
1115        });
1116        let response = self
1117            .post(&urls::query_auth(config.as_ref()), &body.to_string())
1118            .await?;
1119        let query_auth: WxOpenQueryAuthResult =
1120            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1121        // Java:authorizer_access_token 非空 → updateAuthorizerAccessToken;
1122        // authorizer_refresh_token 非空 → updateAuthorizerRefreshToken
1123        if let Some(info) = &query_auth.authorization_info {
1124            if let (Some(appid), Some(token)) =
1125                (&info.authorizer_appid, &info.authorizer_access_token)
1126            {
1127                config.update_authorizer_access_token_with_expiry(
1128                    appid,
1129                    token,
1130                    info.expires_in.unwrap_or(0),
1131                );
1132            }
1133            if let (Some(appid), Some(refresh)) =
1134                (&info.authorizer_appid, &info.authorizer_refresh_token)
1135            {
1136                config.update_authorizer_refresh_token(appid, refresh);
1137            }
1138        }
1139        Ok(query_auth)
1140    }
1141
1142    async fn get_authorizer_info(
1143        &self,
1144        authorizer_appid: &str,
1145    ) -> Result<WxOpenAuthorizerInfoResult, WxErrorException> {
1146        let svc = self.svc()?;
1147        let config = svc.wx_open_config_storage();
1148        let body = serde_json::json!({
1149            "component_appid": config.component_app_id().unwrap_or_default(),
1150            "authorizer_appid": authorizer_appid,
1151        });
1152        let response = self
1153            .post(
1154                &urls::get_authorizer_info(config.as_ref()),
1155                &body.to_string(),
1156            )
1157            .await?;
1158        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1159    }
1160
1161    async fn get_authorizer_list(
1162        &self,
1163        begin: i32,
1164        len: i32,
1165    ) -> Result<WxOpenAuthorizerListResult, WxErrorException> {
1166        // Java `begin = Math.max(begin, 0); len = len == 0 ? 10 : len;`
1167        let begin = begin.max(0);
1168        let len = if len == 0 { 10 } else { len };
1169        let svc = self.svc()?;
1170        let config = svc.wx_open_config_storage();
1171        let body = serde_json::json!({
1172            "component_appid": config.component_app_id().unwrap_or_default(),
1173            "offset": begin,
1174            "count": len,
1175        });
1176        let response = self
1177            .post(
1178                &urls::get_authorizer_list(config.as_ref()),
1179                &body.to_string(),
1180            )
1181            .await?;
1182        let ret: WxOpenAuthorizerListResult =
1183            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1184        // Java:逐条回写 authorizer_appid/refresh_token 到配置存储
1185        if let Some(list) = &ret.list {
1186            for data in list {
1187                if let (Some(appid), Some(refresh)) =
1188                    (data.get("authorizer_appid"), data.get("refresh_token"))
1189                {
1190                    config.update_authorizer_refresh_token(appid, refresh);
1191                }
1192            }
1193        }
1194        Ok(ret)
1195    }
1196
1197    async fn get_authorizer_option(
1198        &self,
1199        authorizer_appid: &str,
1200        option_name: &str,
1201    ) -> Result<WxOpenAuthorizerOptionResult, WxErrorException> {
1202        // Java:授权方 access_token 为 key,经 post(uri, json, "access_token",
1203        // authorizerAccessToken) 发送
1204        let authorizer_access_token = self
1205            .get_authorizer_access_token(authorizer_appid, false)
1206            .await?;
1207        let svc = self.svc()?;
1208        let config = svc.wx_open_config_storage();
1209        let body = serde_json::json!({
1210            "component_appid": config.component_app_id().unwrap_or_default(),
1211            "authorizer_appid": authorizer_appid,
1212            "option_name": option_name,
1213        });
1214        let response = self
1215            .post_with_token(
1216                &urls::get_authorizer_option(config.as_ref()),
1217                &body.to_string(),
1218                "access_token",
1219                &authorizer_access_token,
1220            )
1221            .await?;
1222        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1223    }
1224
1225    async fn set_authorizer_option(
1226        &self,
1227        authorizer_appid: &str,
1228        option_name: &str,
1229        option_value: &str,
1230    ) -> Result<(), WxErrorException> {
1231        let authorizer_access_token = self
1232            .get_authorizer_access_token(authorizer_appid, false)
1233            .await?;
1234        let svc = self.svc()?;
1235        let config = svc.wx_open_config_storage();
1236        let body = serde_json::json!({
1237            "component_appid": config.component_app_id().unwrap_or_default(),
1238            "authorizer_appid": authorizer_appid,
1239            "option_name": option_name,
1240            "option_value": option_value,
1241        });
1242        self.post_with_token(
1243            &urls::set_authorizer_option(config.as_ref()),
1244            &body.to_string(),
1245            "access_token",
1246            &authorizer_access_token,
1247        )
1248        .await?;
1249        Ok(())
1250    }
1251
1252    async fn get_authorizer_access_token(
1253        &self,
1254        app_id: &str,
1255        force_refresh: bool,
1256    ) -> Result<String, WxErrorException> {
1257        let svc = self.svc()?;
1258        let config = svc.wx_open_config_storage();
1259        if !force_refresh && !config.is_authorizer_access_token_expired(app_id) {
1260            return config
1261                .authorizer_access_token(app_id)
1262                .ok_or_else(|| WxErrorException::from_code(-99, "authorizer access token 为空"));
1263        }
1264        // 对应 Java `config.getWxMpConfigStorage(appId).getAccessTokenLock()`
1265        // (按 appId 分桶锁);Java tryLock(100ms) 轮询 + 每轮重查缓存
1266        let lock = config.lock_by_key(app_id);
1267        let _guard = loop {
1268            if !force_refresh && !config.is_authorizer_access_token_expired(app_id) {
1269                return config.authorizer_access_token(app_id).ok_or_else(|| {
1270                    WxErrorException::from_code(-99, "authorizer access token 为空")
1271                });
1272            }
1273            match lock.try_lock() {
1274                Ok(guard) => break guard,
1275                Err(_) => tokio::time::sleep(Duration::from_millis(100)).await,
1276            }
1277        };
1278
1279        let body = serde_json::json!({
1280            "component_appid": config.component_app_id().unwrap_or_default(),
1281            "authorizer_appid": app_id,
1282            "authorizer_refresh_token": config.authorizer_refresh_token(app_id).unwrap_or_default(),
1283        });
1284        let response = self
1285            .post(
1286                &api_authorizer_token_url(config.as_ref()),
1287                &body.to_string(),
1288            )
1289            .await?;
1290        let token: WxOpenAuthorizerAccessToken =
1291            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1292        config.update_authorizer_access_token(app_id, &token);
1293        config.update_authorizer_refresh_token(app_id, token.authorizer_refresh_token());
1294        config
1295            .authorizer_access_token(app_id)
1296            .ok_or_else(|| WxErrorException::from_code(-99, "authorizer access token 为空"))
1297    }
1298
1299    async fn oauth2_get_access_token(
1300        &self,
1301        app_id: &str,
1302        code: &str,
1303    ) -> Result<WxOAuth2AccessToken, WxErrorException> {
1304        let svc = self.svc()?;
1305        let config = svc.wx_open_config_storage();
1306        let url = urls::oauth2_access_token(config.as_ref(), app_id, code);
1307        let response = self.get(&url).await?;
1308        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1309    }
1310
1311    async fn oauth2_refresh_access_token(
1312        &self,
1313        app_id: &str,
1314        refresh_token: &str,
1315    ) -> Result<WxOAuth2AccessToken, WxErrorException> {
1316        let svc = self.svc()?;
1317        let config = svc.wx_open_config_storage();
1318        let url = urls::oauth2_refresh_token(config.as_ref(), app_id, refresh_token);
1319        let response = self.get(&url).await?;
1320        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1321    }
1322
1323    async fn miniapp_jscode2_session(
1324        &self,
1325        app_id: &str,
1326        js_code: &str,
1327    ) -> Result<serde_json::Value, WxErrorException> {
1328        let svc = self.svc()?;
1329        let config = svc.wx_open_config_storage();
1330        let url = urls::miniapp_jscode2_session(config.as_ref(), app_id, js_code);
1331        let response = self.get(&url).await?;
1332        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1333    }
1334
1335    async fn get_template_draft_list(
1336        &self,
1337    ) -> Result<Option<Vec<WxOpenMaCodeTemplate>>, WxErrorException> {
1338        let svc = self.svc()?;
1339        let config = svc.wx_open_config_storage();
1340        // Java `get(GET_TEMPLATE_DRAFT_LIST_URL, "access_token")`
1341        let response = self
1342            .get_with_key(
1343                &urls::get_template_draft_list(config.as_ref()),
1344                "access_token",
1345            )
1346            .await?;
1347        let json: serde_json::Value =
1348            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1349        if let Some(list) = json.get("draft_list") {
1350            serde_json::from_value(list.clone())
1351                .map(Some)
1352                .map_err(|e| WxErrorException::Serde(e.to_string()))
1353        } else {
1354            // Java 无 draft_list 返回 null
1355            Ok(None)
1356        }
1357    }
1358
1359    async fn get_template_list(
1360        &self,
1361    ) -> Result<Option<Vec<WxOpenMaCodeTemplate>>, WxErrorException> {
1362        self.get_template_list_with_type(None).await
1363    }
1364
1365    async fn get_template_list_with_type(
1366        &self,
1367        template_type: Option<i32>,
1368    ) -> Result<Option<Vec<WxOpenMaCodeTemplate>>, WxErrorException> {
1369        let svc = self.svc()?;
1370        let config = svc.wx_open_config_storage();
1371        // Java `GET_TEMPLATE_LIST_URL + (templateType == null ? "" :
1372        // "?template_type=" + templateType)`
1373        let url = match template_type {
1374            Some(t) => format!(
1375                "{}?template_type={t}",
1376                urls::get_template_list(config.as_ref())
1377            ),
1378            None => urls::get_template_list(config.as_ref()),
1379        };
1380        let response = self.get_with_key(&url, "access_token").await?;
1381        let json: serde_json::Value =
1382            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1383        if let Some(list) = json.get("template_list") {
1384            serde_json::from_value(list.clone())
1385                .map(Some)
1386                .map_err(|e| WxErrorException::Serde(e.to_string()))
1387        } else {
1388            Ok(None)
1389        }
1390    }
1391
1392    async fn add_to_template(&self, draft_id: i64) -> Result<(), WxErrorException> {
1393        let svc = self.svc()?;
1394        let config = svc.wx_open_config_storage();
1395        let body = serde_json::json!({ "draft_id": draft_id });
1396        self.post_with_key(
1397            &urls::add_to_template(config.as_ref()),
1398            &body.to_string(),
1399            "access_token",
1400        )
1401        .await?;
1402        Ok(())
1403    }
1404
1405    async fn add_to_template_with_type(
1406        &self,
1407        draft_id: i64,
1408        template_type: i32,
1409    ) -> Result<(), WxErrorException> {
1410        let svc = self.svc()?;
1411        let config = svc.wx_open_config_storage();
1412        let body = serde_json::json!({ "draft_id": draft_id, "template_type": template_type });
1413        self.post_with_key(
1414            &urls::add_to_template(config.as_ref()),
1415            &body.to_string(),
1416            "access_token",
1417        )
1418        .await?;
1419        Ok(())
1420    }
1421
1422    async fn delete_template(&self, template_id: i64) -> Result<(), WxErrorException> {
1423        let svc = self.svc()?;
1424        let config = svc.wx_open_config_storage();
1425        let body = serde_json::json!({ "template_id": template_id });
1426        self.post_with_key(
1427            &urls::delete_template(config.as_ref()),
1428            &body.to_string(),
1429            "access_token",
1430        )
1431        .await?;
1432        Ok(())
1433    }
1434
1435    async fn create_open_account(
1436        &self,
1437        app_id: &str,
1438        app_id_type: &str,
1439    ) -> Result<WxOpenCreateResult, WxErrorException> {
1440        // Java `param.addProperty("appid", appId)` 后经
1441        // `openAccountServicePost(appId, appIdType, CREATE_OPEN_URL, param)`
1442        let svc = self.svc()?;
1443        let config = svc.wx_open_config_storage();
1444        let param = serde_json::json!({ "appid": app_id });
1445        let json = self
1446            .open_account_service_post(
1447                app_id,
1448                app_id_type,
1449                &urls::create_open(config.as_ref()),
1450                &param,
1451            )
1452            .await?;
1453        // Java `WxOpenCreateResult.fromJson(json)`;errcode 先经
1454        // normalize_errcode 归一化为字符串(Gson 宽松转换语义,ADAPTED)
1455        let json = Self::normalize_errcode(&json)?;
1456        WxOpenCreateResult::from_json(&json).map_err(WxErrorException::Serde)
1457    }
1458
1459    async fn bind_open_account(
1460        &self,
1461        app_id: &str,
1462        app_id_type: &str,
1463        open_appid: &str,
1464    ) -> Result<bool, WxErrorException> {
1465        // Java `param.addProperty("appid", ...) + addProperty("open_appid", ...)`
1466        let svc = self.svc()?;
1467        let config = svc.wx_open_config_storage();
1468        let param = serde_json::json!({ "appid": app_id, "open_appid": open_appid });
1469        let json = self
1470            .open_account_service_post(
1471                app_id,
1472                app_id_type,
1473                &urls::bind_open(config.as_ref()),
1474                &param,
1475            )
1476            .await?;
1477        // Java `WxOpenResult.fromJson(json).isSuccess()`(errcode 忽略大小写
1478        // 等于 "0";errcode 先归一化为字符串,Gson 宽松转换语义,ADAPTED;
1479        // 缺失时 "" → false)
1480        let json = Self::normalize_errcode(&json)?;
1481        Ok(WxOpenResult::from_json(&json)
1482            .map_err(WxErrorException::Serde)?
1483            .errcode
1484            .eq_ignore_ascii_case("0"))
1485    }
1486
1487    async fn unbind_open_account(
1488        &self,
1489        app_id: &str,
1490        app_id_type: &str,
1491        open_appid: &str,
1492    ) -> Result<bool, WxErrorException> {
1493        let svc = self.svc()?;
1494        let config = svc.wx_open_config_storage();
1495        let param = serde_json::json!({ "appid": app_id, "open_appid": open_appid });
1496        let json = self
1497            .open_account_service_post(
1498                app_id,
1499                app_id_type,
1500                &urls::unbind_open(config.as_ref()),
1501                &param,
1502            )
1503            .await?;
1504        let json = Self::normalize_errcode(&json)?;
1505        Ok(WxOpenResult::from_json(&json)
1506            .map_err(WxErrorException::Serde)?
1507            .errcode
1508            .eq_ignore_ascii_case("0"))
1509    }
1510
1511    async fn get_open_account(
1512        &self,
1513        app_id: &str,
1514        app_id_type: &str,
1515    ) -> Result<WxOpenGetResult, WxErrorException> {
1516        let svc = self.svc()?;
1517        let config = svc.wx_open_config_storage();
1518        let param = serde_json::json!({ "appid": app_id });
1519        let json = self
1520            .open_account_service_post(
1521                app_id,
1522                app_id_type,
1523                &urls::get_open(config.as_ref()),
1524                &param,
1525            )
1526            .await?;
1527        // Java `WxOpenGetResult.fromJson(json)`;errcode 归一化同
1528        // create_open_account(ADAPTED)
1529        let json = Self::normalize_errcode(&json)?;
1530        WxOpenGetResult::from_json(&json).map_err(WxErrorException::Serde)
1531    }
1532
1533    async fn have_open(&self) -> Result<WxOpenHaveResult, WxErrorException> {
1534        let svc = self.svc()?;
1535        let config = svc.wx_open_config_storage();
1536        // Java `get(HAVE_OPEN_URL, "access_token")`
1537        let response = self
1538            .get_with_key(&urls::have_open(config.as_ref()), "access_token")
1539            .await?;
1540        WxOpenHaveResult::from_json(&response).map_err(WxErrorException::Serde)
1541    }
1542
1543    async fn fast_register_weapp(
1544        &self,
1545        name: &str,
1546        code: &str,
1547        code_type: &str,
1548        legal_persona_wechat: &str,
1549        legal_persona_name: &str,
1550        component_phone: &str,
1551    ) -> Result<WxOpenResult, WxErrorException> {
1552        let svc = self.svc()?;
1553        let config = svc.wx_open_config_storage();
1554        let body = serde_json::json!({
1555            "name": name,
1556            "code": code,
1557            "code_type": code_type,
1558            "legal_persona_wechat": legal_persona_wechat,
1559            "legal_persona_name": legal_persona_name,
1560            "component_phone": component_phone,
1561        });
1562        // Java `post(..., "component_access_token")`(默认键,逐字镜像)
1563        let response = self
1564            .post_with_key(
1565                &urls::fast_register_weapp(config.as_ref()),
1566                &body.to_string(),
1567                "component_access_token",
1568            )
1569            .await?;
1570        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1571    }
1572
1573    async fn fast_register_weapp_search(
1574        &self,
1575        name: &str,
1576        legal_persona_wechat: &str,
1577        legal_persona_name: &str,
1578    ) -> Result<WxOpenResult, WxErrorException> {
1579        let svc = self.svc()?;
1580        let config = svc.wx_open_config_storage();
1581        let body = serde_json::json!({
1582            "name": name,
1583            "legal_persona_wechat": legal_persona_wechat,
1584            "legal_persona_name": legal_persona_name,
1585        });
1586        let response = self
1587            .post_with_key(
1588                &urls::fast_register_weapp_search(config.as_ref()),
1589                &body.to_string(),
1590                "component_access_token",
1591            )
1592            .await?;
1593        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1594    }
1595
1596    async fn fast_register_personal_weapp(
1597        &self,
1598        idname: &str,
1599        wxuser: &str,
1600        component_phone: &str,
1601    ) -> Result<WxOpenRegisterPersonalWeappResult, WxErrorException> {
1602        let svc = self.svc()?;
1603        let config = svc.wx_open_config_storage();
1604        let body = serde_json::json!({
1605            "idname": idname,
1606            "wxuser": wxuser,
1607            "component_phone": component_phone,
1608        });
1609        let response = self
1610            .post_with_key(
1611                &urls::fast_register_personal_weapp(config.as_ref()),
1612                &body.to_string(),
1613                "component_access_token",
1614            )
1615            .await?;
1616        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1617    }
1618
1619    async fn fast_register_personal_weapp_search(
1620        &self,
1621        taskid: &str,
1622    ) -> Result<WxOpenRegisterPersonalWeappResult, WxErrorException> {
1623        let svc = self.svc()?;
1624        let config = svc.wx_open_config_storage();
1625        let body = serde_json::json!({ "taskid": taskid });
1626        let response = self
1627            .post_with_key(
1628                &urls::fast_register_personal_weapp_search(config.as_ref()),
1629                &body.to_string(),
1630                "component_access_token",
1631            )
1632            .await?;
1633        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1634    }
1635
1636    async fn fast_register_beta_weapp(
1637        &self,
1638        name: &str,
1639        openid: &str,
1640    ) -> Result<WxOpenRegisterBetaWeappResult, WxErrorException> {
1641        let svc = self.svc()?;
1642        let config = svc.wx_open_config_storage();
1643        let body = serde_json::json!({ "name": name, "openid": openid });
1644        // Java `post(..., "access_token")`
1645        let response = self
1646            .post_with_key(
1647                &urls::fast_register_beta_weapp(config.as_ref()),
1648                &body.to_string(),
1649                "access_token",
1650            )
1651            .await?;
1652        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1653    }
1654
1655    async fn register_shop(
1656        &self,
1657        wx_name: &str,
1658        id_card_name: &str,
1659        id_card_number: &str,
1660        channel_id: Option<&str>,
1661        api_openstore_type: Option<i32>,
1662        auth_page_url: Option<&str>,
1663    ) -> Result<WxOpenResult, WxErrorException> {
1664        let svc = self.svc()?;
1665        let config = svc.wx_open_config_storage();
1666        let mut body = serde_json::json!({
1667            "wx_name": wx_name,
1668            "id_card_name": id_card_name,
1669            "id_card_number": id_card_number,
1670            "api_openstore_type": api_openstore_type,
1671        });
1672        if let Some(channel_id) = channel_id {
1673            if !channel_id.is_empty() {
1674                body["channel_id"] = serde_json::json!(channel_id);
1675            }
1676        }
1677        if let Some(auth_page_url) = auth_page_url {
1678            if !auth_page_url.is_empty() {
1679                body["auth_page_url"] = serde_json::json!(auth_page_url);
1680            }
1681        }
1682        let response = self
1683            .post_with_key(
1684                &urls::register_shop(config.as_ref()),
1685                &body.to_string(),
1686                "component_access_token",
1687            )
1688            .await?;
1689        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1690    }
1691
1692    async fn check_audit_status(&self, wx_name: &str) -> Result<String, WxErrorException> {
1693        let svc = self.svc()?;
1694        let config = svc.wx_open_config_storage();
1695        // Java:URL 先拼 component_access_token,再经组件层 post(会再次
1696        // 注入 component_access_token,双 token 线格式镜像)
1697        let url = format!(
1698            "{}?access_token={}",
1699            urls::check_shop_audit_status(config.as_ref()),
1700            self.get_component_access_token(false).await?
1701        );
1702        let body = serde_json::json!({ "wx_name": wx_name });
1703        self.post(&url, &body.to_string()).await
1704    }
1705
1706    async fn check_audit_status_with_appid(
1707        &self,
1708        app_id: &str,
1709        wx_name: &str,
1710    ) -> Result<String, WxErrorException> {
1711        let svc = self.svc()?;
1712        let config = svc.wx_open_config_storage();
1713        let url = format!(
1714            "{}?access_token={}",
1715            urls::check_shop_audit_status(config.as_ref()),
1716            self.get_authorizer_access_token(app_id, false).await?
1717        );
1718        let body = serde_json::json!({ "wx_name": wx_name });
1719        self.post(&url, &body.to_string()).await
1720    }
1721
1722    async fn submit_merchant_info(
1723        &self,
1724        app_id: &str,
1725        subject_type: &str,
1726        busi_license: &MinishopBusiLicense,
1727        organization_code_info: Option<&MinishopOrganizationCodeInfo>,
1728        idcard_info: Option<&MinishopIdcardInfo>,
1729        super_administrator_info: Option<&MinishopSuperAdministratorInfo>,
1730        merchant_shortname: Option<&str>,
1731    ) -> Result<WxOpenResult, WxErrorException> {
1732        let svc = self.svc()?;
1733        let config = svc.wx_open_config_storage();
1734        let mut body = serde_json::json!({
1735            "app_id": app_id,
1736            "subject_type": subject_type,
1737            "busi_license": minishop_json::busi_license(busi_license),
1738        });
1739        if let Some(o) = organization_code_info {
1740            body["organization_code_info"] = minishop_json::organization_code_info(o);
1741        }
1742        if let Some(i) = idcard_info {
1743            body["id_card_info"] = minishop_json::idcard_info(i);
1744        }
1745        if let Some(s) = super_administrator_info {
1746            body["super_administrator_info"] = minishop_json::super_administrator_info(s);
1747        }
1748        if let Some(name) = merchant_shortname {
1749            if !name.is_empty() {
1750                body["merchant_shortname"] = serde_json::json!(name);
1751            }
1752        }
1753        // Java `getWxOpenService().post(url, json)`(裸 post,URL 已带
1754        // 授权方 access_token)
1755        let url = format!(
1756            "{}?access_token={}",
1757            urls::submit_merchant_info(config.as_ref()),
1758            self.get_authorizer_access_token(app_id, false).await?
1759        );
1760        let response = self.bare_post(&url, &body.to_string()).await?;
1761        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1762    }
1763
1764    async fn submit_basic_info(
1765        &self,
1766        app_id: &str,
1767        name_info: &MinishopNameInfo,
1768        return_info: &MinishopReturnInfo,
1769    ) -> Result<WxOpenResult, WxErrorException> {
1770        let svc = self.svc()?;
1771        let config = svc.wx_open_config_storage();
1772        let body = serde_json::json!({
1773            "appid": app_id,
1774            "name_info": minishop_json::name_info(name_info),
1775            "return_info": minishop_json::return_info(return_info),
1776        });
1777        let url = format!(
1778            "{}?access_token={}",
1779            urls::submit_basic_info(config.as_ref()),
1780            self.get_authorizer_access_token(app_id, false).await?
1781        );
1782        let response = self.bare_post(&url, &body.to_string()).await?;
1783        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
1784    }
1785
1786    async fn upload_minishop_image_pic_file(
1787        &self,
1788        app_id: &str,
1789        height: i32,
1790        width: i32,
1791        file_path: &str,
1792    ) -> Result<WxMinishopImageUploadResult, WxErrorException> {
1793        let svc = self.svc()?;
1794        let config = svc.wx_open_config_storage();
1795        // Java `WxOpenMinishopService.UPLOAD_IMG_MINISHOP_FILE_URL + "?access_token="
1796        // + getAuthorizerAccessToken(appId, false) + "&height=" + height +
1797        // "&width=" + width`;multipart 上传经门面 uploadMinishopMediaFile
1798        let url = format!(
1799            "{}?access_token={}&height={}&width={}",
1800            urls::upload_image(config.as_ref()),
1801            self.get_authorizer_access_token(app_id, false).await?,
1802            height,
1803            width,
1804        );
1805        svc.upload_minishop_media_file(&url, file_path).await
1806    }
1807
1808    async fn get_minishop_categories(
1809        &self,
1810        app_id: &str,
1811        f_cat_id: i32,
1812    ) -> Result<MinishopCategories, WxErrorException> {
1813        let svc = self.svc()?;
1814        let config = svc.wx_open_config_storage();
1815        let body = serde_json::json!({ "f_cat_id": f_cat_id });
1816        let url = format!(
1817            "{}?access_token={}",
1818            urls::minishop_category_get(config.as_ref()),
1819            self.get_authorizer_access_token(app_id, false).await?
1820        );
1821        let response = self.bare_post(&url, &body.to_string()).await?;
1822        let resp: serde_json::Value =
1823            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1824        let mut categories = MinishopCategories::default();
1825        categories.errcode = Self::json_i64(&resp, "errcode") as i32;
1826        if categories.errcode == 0 {
1827            // Java `if (catListJson != null || !catListJson.isEmpty())` 后遍历
1828            if let Some(list) = resp.get("cat_list").and_then(|v| v.as_array()) {
1829                for item in list {
1830                    let mut c = MinishopCategory::default();
1831                    c.cat_id = Self::json_i64(item, "cat_id") as i32;
1832                    c.f_cat_id = Self::json_i64(item, "f_cat_id") as i32;
1833                    c.name = Self::json_str(item, "name");
1834                    categories.cat_list.push(c);
1835                }
1836            }
1837        } else {
1838            categories.errmsg = Self::json_str(&resp, "errmsg");
1839        }
1840        Ok(categories)
1841    }
1842
1843    async fn get_minishop_brands(
1844        &self,
1845        app_id: &str,
1846    ) -> Result<MinishopBrandList, WxErrorException> {
1847        let svc = self.svc()?;
1848        let config = svc.wx_open_config_storage();
1849        let url = format!(
1850            "{}?access_token={}",
1851            urls::minishop_brand_get(config.as_ref()),
1852            self.get_authorizer_access_token(app_id, false).await?
1853        );
1854        let response = self.bare_post(&url, "{}").await?;
1855        let resp: serde_json::Value =
1856            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1857        let mut brand_list = MinishopBrandList::default();
1858        brand_list.errcode = Self::json_i64(&resp, "errcode") as i32;
1859        if brand_list.errcode == 0 {
1860            if let Some(list) = resp.get("brands").and_then(|v| v.as_array()) {
1861                for item in list {
1862                    let mut brand = MinishopBrand::default();
1863                    brand.first_cat_id = Self::json_i64(item, "first_cat_id") as i32;
1864                    brand.second_cat_id = Self::json_i64(item, "second_cat_id") as i32;
1865                    brand.third_cat_id = Self::json_i64(item, "third_cat_id") as i32;
1866                    if let Some(info) = item.get("brand_info") {
1867                        brand.brand_info.brand_id = Self::json_i64(info, "brand_id") as i32;
1868                        brand.brand_info.brand_name = Self::json_str(info, "brand_name");
1869                    }
1870                    brand_list.brands.push(brand);
1871                }
1872            }
1873        } else {
1874            brand_list.errmsg = Self::json_str(&resp, "errmsg");
1875        }
1876        Ok(brand_list)
1877    }
1878
1879    async fn get_minishop_delivery_template(
1880        &self,
1881        app_id: &str,
1882    ) -> Result<MinishopDeliveryTemplateResult, WxErrorException> {
1883        let svc = self.svc()?;
1884        let config = svc.wx_open_config_storage();
1885        let url = format!(
1886            "{}?access_token={}",
1887            urls::minishop_delivery_template_get(config.as_ref()),
1888            self.get_authorizer_access_token(app_id, false).await?
1889        );
1890        let response = self.bare_post(&url, "{}").await?;
1891        let resp: serde_json::Value =
1892            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1893        let mut result = MinishopDeliveryTemplateResult::default();
1894        result.err_code = Self::json_i64(&resp, "errcode") as i32;
1895        if result.err_code == 0 {
1896            if let Some(list) = resp.get("template_list").and_then(|v| v.as_array()) {
1897                for item in list {
1898                    let mut template = MinishopDeliveryTemplate::default();
1899                    template.template_id = Self::json_i64(item, "template_id") as i32;
1900                    template.name = Self::json_str(item, "name");
1901                    // Java `valuation_type == 1 ? WEIGHT : PACKAGE`
1902                    template.valuation_type = if Self::json_i64(item, "valuation_type") == 1 {
1903                        ValuationType::Weight
1904                    } else {
1905                        ValuationType::Package
1906                    };
1907                    result.template_list.push(template);
1908                }
1909            }
1910        } else {
1911            result.err_msg = Self::json_str(&resp, "errmsg");
1912        }
1913        Ok(result)
1914    }
1915
1916    async fn get_minishop_cat_list(
1917        &self,
1918        app_id: &str,
1919    ) -> Result<MinishopShopCatList, WxErrorException> {
1920        let svc = self.svc()?;
1921        let config = svc.wx_open_config_storage();
1922        let url = format!(
1923            "{}?access_token={}",
1924            urls::minishop_shopcategory_get(config.as_ref()),
1925            self.get_authorizer_access_token(app_id, false).await?
1926        );
1927        let response = self.bare_post(&url, "{}").await?;
1928        let resp: serde_json::Value =
1929            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1930        let mut shop_cat_list = MinishopShopCatList::default();
1931        shop_cat_list.errcode = Self::json_i64(&resp, "errcode") as i32;
1932        if shop_cat_list.errcode == 0 {
1933            if let Some(list) = resp.get("shopcat_list").and_then(|v| v.as_array()) {
1934                for item in list {
1935                    let mut cat = MinishopShopCat::default();
1936                    cat.shop_cat_id = Self::json_i64(item, "shopcat_id") as i32;
1937                    cat.shop_cat_name = Self::json_str(item, "shopcat_name");
1938                    cat.f_shop_cat_id = Self::json_i64(item, "f_shopcat_id") as i32;
1939                    cat.cat_level = Self::json_i64(item, "cat_level") as i32;
1940                    shop_cat_list.shop_cat_list.push(cat);
1941                }
1942            }
1943        } else {
1944            shop_cat_list.errmsg = Self::json_str(&resp, "errmsg");
1945        }
1946        Ok(shop_cat_list)
1947    }
1948
1949    async fn get_minishop_delivery_company(
1950        &self,
1951        app_id: &str,
1952    ) -> Result<WxMinishopAddGoodsSpuResult, WxErrorException> {
1953        let svc = self.svc()?;
1954        let config = svc.wx_open_config_storage();
1955        let url = format!(
1956            "{}?access_token={}",
1957            urls::minishop_get_delivery_company(config.as_ref()),
1958            self.get_authorizer_access_token(app_id, false).await?
1959        );
1960        let response = self.bare_post(&url, "{}").await?;
1961        let resp: serde_json::Value =
1962            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1963        let mut result = WxMinishopAddGoodsSpuResult::default();
1964        result.errcode = Self::json_i64(&resp, "errcode") as i32;
1965        if result.errcode == 0 {
1966            // Java `result.setData(companies)`:data 为 company_list 数组
1967            result.data = resp.get("company_list").cloned().unwrap_or_default();
1968        } else {
1969            result.errmsg = Self::json_str(&resp, "errmsg");
1970        }
1971        Ok(result)
1972    }
1973
1974    async fn minishop_create_coupon(
1975        &self,
1976        app_id: &str,
1977        coupon_info: &WxMinishopCoupon,
1978    ) -> Result<i32, WxErrorException> {
1979        let svc = self.svc()?;
1980        let config = svc.wx_open_config_storage();
1981        // Java `getAuthorizerAccessToken(appId, true)`(强制刷新)
1982        let url = format!(
1983            "{}?access_token={}",
1984            urls::minishop_create_coupon(config.as_ref()),
1985            self.get_authorizer_access_token(app_id, true).await?
1986        );
1987        let response = self
1988            .bare_post(&url, &minishop_json::coupon(coupon_info).to_string())
1989            .await?;
1990        let resp: serde_json::Value =
1991            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
1992        // Java:errcode==0 → data.coupon_id,否则 -1
1993        let coupon_id = if Self::json_i64(&resp, "errcode") == 0 {
1994            resp.get("data")
1995                .map(|d| Self::json_i64(d, "coupon_id") as i32)
1996                .unwrap_or(-1)
1997        } else {
1998            -1
1999        };
2000        Ok(coupon_id)
2001    }
2002
2003    async fn minishop_get_coupon_list(
2004        &self,
2005        app_id: &str,
2006        _start_create_time: &str,
2007        _end_create_time: &str,
2008        _status: i32,
2009        _page: i32,
2010        _page_size: i32,
2011    ) -> Result<Option<WxMinishopCouponStock>, WxErrorException> {
2012        // Java 实现先取授权方 token(forceRefresh=true)后 `return null`
2013        let svc = self.svc()?;
2014        let config = svc.wx_open_config_storage();
2015        let _url = format!(
2016            "{}?access_token={}",
2017            urls::minishop_get_coupon_list(config.as_ref()),
2018            self.get_authorizer_access_token(app_id, true).await?
2019        );
2020        Ok(None)
2021    }
2022
2023    async fn minishop_push_coupon_to_user(
2024        &self,
2025        app_id: &str,
2026        open_id: &str,
2027        coupon_id: i32,
2028    ) -> Result<WxOpenResult, WxErrorException> {
2029        let svc = self.svc()?;
2030        let config = svc.wx_open_config_storage();
2031        let url = format!(
2032            "{}?access_token={}",
2033            urls::minishop_push_coupon(config.as_ref()),
2034            self.get_authorizer_access_token(app_id, true).await?
2035        );
2036        let body = serde_json::json!({ "openid": open_id, "coupon_id": coupon_id });
2037        let response = self.bare_post(&url, &body.to_string()).await?;
2038        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2039    }
2040
2041    async fn minishop_update_coupon(
2042        &self,
2043        app_id: &str,
2044        coupon_info: &WxMinishopCoupon,
2045    ) -> Result<i32, WxErrorException> {
2046        let svc = self.svc()?;
2047        let config = svc.wx_open_config_storage();
2048        let url = format!(
2049            "{}?access_token={}",
2050            urls::minishop_update_coupon(config.as_ref()),
2051            self.get_authorizer_access_token(app_id, true).await?
2052        );
2053        let response = self
2054            .bare_post(&url, &minishop_json::coupon(coupon_info).to_string())
2055            .await?;
2056        let resp: serde_json::Value =
2057            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
2058        let coupon_id = if Self::json_i64(&resp, "errcode") == 0 {
2059            resp.get("data")
2060                .map(|d| Self::json_i64(d, "coupon_id") as i32)
2061                .unwrap_or(-1)
2062        } else {
2063            -1
2064        };
2065        Ok(coupon_id)
2066    }
2067
2068    async fn minishop_update_coupon_status(
2069        &self,
2070        app_id: &str,
2071        coupon_id: i32,
2072        status: i32,
2073    ) -> Result<WxOpenResult, WxErrorException> {
2074        let svc = self.svc()?;
2075        let config = svc.wx_open_config_storage();
2076        let url = format!(
2077            "{}?access_token={}",
2078            urls::minishop_update_coupon_status(config.as_ref()),
2079            self.get_authorizer_access_token(app_id, true).await?
2080        );
2081        let body = serde_json::json!({ "coupon_id": coupon_id, "status": status });
2082        let response = self.bare_post(&url, &body.to_string()).await?;
2083        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2084    }
2085
2086    async fn minishop_goods_add_spu(
2087        &self,
2088        app_id: &str,
2089        spu: &WxMinishopSpu,
2090    ) -> Result<WxMinishopAddGoodsSpuResult, WxErrorException> {
2091        self.minishop_spu_common(app_id, urls::minishop_add_spu, spu, "create_time")
2092            .await
2093    }
2094
2095    async fn minishop_goods_del_spu(
2096        &self,
2097        app_id: &str,
2098        product_id: i64,
2099        out_product_id: i64,
2100    ) -> Result<WxOpenResult, WxErrorException> {
2101        let svc = self.svc()?;
2102        let config = svc.wx_open_config_storage();
2103        let url = format!(
2104            "{}?access_token={}",
2105            urls::minishop_del_spu(config.as_ref()),
2106            self.get_authorizer_access_token(app_id, true).await?
2107        );
2108        // Java `outProductId.toString()`
2109        let body = serde_json::json!({ "product_id": product_id, "out_product_id": out_product_id.to_string() });
2110        let response = self.bare_post(&url, &body.to_string()).await?;
2111        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2112    }
2113
2114    async fn minishop_goods_update_spu(
2115        &self,
2116        app_id: &str,
2117        spu: &WxMinishopSpu,
2118    ) -> Result<WxMinishopAddGoodsSpuResult, WxErrorException> {
2119        self.minishop_spu_common(app_id, urls::minishop_update_spu, spu, "update_time")
2120            .await
2121    }
2122
2123    async fn minishop_goods_listing_spu(
2124        &self,
2125        app_id: &str,
2126        product_id: i64,
2127        out_product_id: i64,
2128    ) -> Result<WxOpenResult, WxErrorException> {
2129        let svc = self.svc()?;
2130        let config = svc.wx_open_config_storage();
2131        let url = format!(
2132            "{}?access_token={}",
2133            urls::minishop_listing_spu(config.as_ref()),
2134            self.get_authorizer_access_token(app_id, true).await?
2135        );
2136        let body = serde_json::json!({ "product_id": product_id, "out_product_id": out_product_id.to_string() });
2137        let response = self.bare_post(&url, &body.to_string()).await?;
2138        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2139    }
2140
2141    async fn minishop_goods_delisting_spu(
2142        &self,
2143        app_id: &str,
2144        product_id: i64,
2145        out_product_id: i64,
2146    ) -> Result<WxOpenResult, WxErrorException> {
2147        let svc = self.svc()?;
2148        let config = svc.wx_open_config_storage();
2149        let url = format!(
2150            "{}?access_token={}",
2151            urls::minishop_delisting_spu(config.as_ref()),
2152            self.get_authorizer_access_token(app_id, true).await?
2153        );
2154        let body = serde_json::json!({ "product_id": product_id, "out_product_id": out_product_id.to_string() });
2155        let response = self.bare_post(&url, &body.to_string()).await?;
2156        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2157    }
2158
2159    async fn minishop_goods_add_sku(
2160        &self,
2161        app_id: &str,
2162        sku: &WxMinishopSku,
2163    ) -> Result<WxMinishopAddGoodsSpuResult, WxErrorException> {
2164        // Java `minishiopGoodsAddSku`:add 接口响应 data{sku_id, create_time}
2165        self.minishop_sku_add_common(app_id, urls::minishop_add_sku, sku)
2166            .await
2167    }
2168
2169    async fn minishop_goods_batch_add_sku(
2170        &self,
2171        app_id: &str,
2172        sku_list: &[WxMinishopSku],
2173    ) -> Result<WxOpenResult, WxErrorException> {
2174        let svc = self.svc()?;
2175        let config = svc.wx_open_config_storage();
2176        let url = format!(
2177            "{}?access_token={}",
2178            urls::minishop_batch_add_sku(config.as_ref()),
2179            self.get_authorizer_access_token(app_id, true).await?
2180        );
2181        let skus: Vec<serde_json::Value> = sku_list.iter().map(minishop_json::sku).collect();
2182        let body = serde_json::json!({ "skus": skus });
2183        let response = self.bare_post(&url, &body.to_string()).await?;
2184        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2185    }
2186
2187    async fn minishop_goods_del_sku(
2188        &self,
2189        app_id: &str,
2190        product_id: i64,
2191        out_product_id: i64,
2192        out_sku_id: &str,
2193        sku_id: i64,
2194    ) -> Result<WxOpenResult, WxErrorException> {
2195        let svc = self.svc()?;
2196        let config = svc.wx_open_config_storage();
2197        let url = format!(
2198            "{}?access_token={}",
2199            urls::minishop_del_sku(config.as_ref()),
2200            self.get_authorizer_access_token(app_id, true).await?
2201        );
2202        let body = serde_json::json!({
2203            "product_id": product_id,
2204            "out_product_id": out_product_id,
2205            "sku_id": sku_id,
2206            "out_sku_id": out_sku_id,
2207        });
2208        let response = self.bare_post(&url, &body.to_string()).await?;
2209        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2210    }
2211
2212    async fn minishop_goods_update_sku(
2213        &self,
2214        app_id: &str,
2215        sku: &WxMinishopSku,
2216    ) -> Result<WxOpenResult, WxErrorException> {
2217        let svc = self.svc()?;
2218        let config = svc.wx_open_config_storage();
2219        let url = format!(
2220            "{}?access_token={}",
2221            urls::minishop_update_sku(config.as_ref()),
2222            self.get_authorizer_access_token(app_id, true).await?
2223        );
2224        let response = self
2225            .bare_post(&url, &minishop_json::sku(sku).to_string())
2226            .await?;
2227        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2228    }
2229
2230    async fn minishop_goods_update_sku_price(
2231        &self,
2232        app_id: &str,
2233        product_id: i64,
2234        out_product_id: i64,
2235        out_sku_id: &str,
2236        sku_id: i64,
2237        _sale_price: i64,
2238        _market_price: i64,
2239    ) -> Result<WxOpenResult, WxErrorException> {
2240        let svc = self.svc()?;
2241        let config = svc.wx_open_config_storage();
2242        let url = format!(
2243            "{}?access_token={}",
2244            urls::minishop_update_sku_price(config.as_ref()),
2245            self.get_authorizer_access_token(app_id, true).await?
2246        );
2247        // 镜像 Java 上游 bug:WxJava 将 sale_price/market_price 均写成
2248        // outSkuId(严格镜像原语义,见 impl 模块文档)
2249        let body = serde_json::json!({
2250            "product_id": product_id,
2251            "out_product_id": out_product_id,
2252            "sku_id": sku_id,
2253            "out_sku_id": out_sku_id,
2254            "sale_price": out_sku_id,
2255            "market_price": out_sku_id,
2256        });
2257        let response = self.bare_post(&url, &body.to_string()).await?;
2258        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2259    }
2260
2261    async fn minishop_goods_update_sku_stock(
2262        &self,
2263        app_id: &str,
2264        product_id: i64,
2265        out_product_id: i64,
2266        out_sku_id: &str,
2267        sku_id: i64,
2268        r#type: i32,
2269        stock_num: i32,
2270    ) -> Result<WxOpenResult, WxErrorException> {
2271        let svc = self.svc()?;
2272        let config = svc.wx_open_config_storage();
2273        let url = format!(
2274            "{}?access_token={}",
2275            urls::minishop_update_sku_stock(config.as_ref()),
2276            self.get_authorizer_access_token(app_id, true).await?
2277        );
2278        let body = serde_json::json!({
2279            "product_id": product_id,
2280            "out_product_id": out_product_id,
2281            "sku_id": sku_id,
2282            "out_sku_id": out_sku_id,
2283            "type": r#type,
2284            "stock_num": stock_num,
2285        });
2286        let response = self.bare_post(&url, &body.to_string()).await?;
2287        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2288    }
2289
2290    async fn minishop_common_post(
2291        &self,
2292        _app_id: &str,
2293        _url: &str,
2294        _request_param: &str,
2295    ) -> Result<Option<String>, WxErrorException> {
2296        // Java 实现 `return null`
2297        Ok(None)
2298    }
2299
2300    async fn add_limit_discount_goods(
2301        &self,
2302        app_id: &str,
2303        limit_discount_goods: &LimitDiscountGoods,
2304    ) -> Result<i32, WxErrorException> {
2305        let svc = self.svc()?;
2306        let config = svc.wx_open_config_storage();
2307        // Java `API_MINISHOP_ADD_LIMIT_DISCOUNT_URL + "access_token=" + ...`
2308        // (无 "?",镜像线格式)
2309        let url = format!(
2310            "{}access_token={}",
2311            urls::minishop_add_limit_discount(config.as_ref()),
2312            self.get_authorizer_access_token(app_id, false).await?
2313        );
2314        let response = self
2315            .bare_post(
2316                &url,
2317                &minishop_json::limit_discount_goods(limit_discount_goods).to_string(),
2318            )
2319            .await?;
2320        let resp: serde_json::Value =
2321            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
2322        // Java:taskId 默认 0,errcode==0 时取 task_id
2323        let task_id = if Self::json_i64(&resp, "errcode") == 0 {
2324            Self::json_i64(&resp, "task_id") as i32
2325        } else {
2326            0
2327        };
2328        Ok(task_id)
2329    }
2330
2331    async fn get_limit_discount_list(
2332        &self,
2333        app_id: &str,
2334        status: Option<i32>,
2335    ) -> Result<Vec<LimitDiscountGoods>, WxErrorException> {
2336        let svc = self.svc()?;
2337        let config = svc.wx_open_config_storage();
2338        let url = format!(
2339            "{}access_token={}",
2340            urls::minishop_get_limit_discount(config.as_ref()),
2341            self.get_authorizer_access_token(app_id, false).await?
2342        );
2343        let mut body = serde_json::json!({});
2344        if let Some(status) = status {
2345            body["status"] = serde_json::json!(status);
2346        }
2347        let response = self.bare_post(&url, &body.to_string()).await?;
2348        let resp: serde_json::Value =
2349            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
2350        let mut list = Vec::new();
2351        if Self::json_i64(&resp, "errcode") == 0 {
2352            if let Some(goods_array) = resp.get("limited_discount_list").and_then(|v| v.as_array())
2353            {
2354                for (i, goods) in goods_array.iter().enumerate() {
2355                    let mut goods_bean = LimitDiscountGoods::default();
2356                    goods_bean.task_id = Self::json_i64(goods, "task_id");
2357                    goods_bean.status = Self::json_i64(goods, "status") as i32;
2358                    // Java `new Date(getAsLong() * 1000)`;Rust bean 以 String
2359                    // 承载(epoch 毫秒字符串,ADAPTED)
2360                    goods_bean.start_time = Self::json_i64(goods, "start_time")
2361                        .saturating_mul(1000)
2362                        .to_string();
2363                    goods_bean.end_time = Self::json_i64(goods, "end_time")
2364                        .saturating_mul(1000)
2365                        .to_string();
2366                    if let Some(sku_array) = goods
2367                        .get("limited_discount_sku_list")
2368                        .and_then(|v| v.as_array())
2369                    {
2370                        for (j, _) in sku_array.iter().enumerate() {
2371                            // 镜像 Java 上游 bug:`skuObj = skuArray.get(i)` 取
2372                            // 外层索引 i 而非 j(严格镜像原语义)
2373                            let sku_json = sku_array.get(i).unwrap_or(&serde_json::Value::Null);
2374                            let mut sku = LimitDiscountSku::default();
2375                            sku.sku_id = Self::json_i64(sku_json, "sku_id");
2376                            // Java `BigDecimal.valueOf(getAsDouble() / 100)`;
2377                            // Rust bean 以 String 承载金额(ADAPTED)
2378                            let price = sku_json
2379                                .get("sale_price")
2380                                .and_then(|v| v.as_f64())
2381                                .map(|p| (p / 100.0).to_string())
2382                                .unwrap_or_default();
2383                            sku.sale_price = price;
2384                            sku.sale_stock = Self::json_i64(sku_json, "sale_stock") as i32;
2385                            let _ = j;
2386                            goods_bean.limit_discount_sku_list.push(sku);
2387                        }
2388                    }
2389                    list.push(goods_bean);
2390                }
2391            }
2392        }
2393        Ok(list)
2394    }
2395
2396    async fn update_limit_discount_status(
2397        &self,
2398        app_id: &str,
2399        task_id: i64,
2400        status: i32,
2401    ) -> Result<WxOpenResult, WxErrorException> {
2402        let svc = self.svc()?;
2403        let config = svc.wx_open_config_storage();
2404        let url = format!(
2405            "{}access_token={}",
2406            urls::minishop_update_limit_discount_status(config.as_ref()),
2407            self.get_authorizer_access_token(app_id, false).await?
2408        );
2409        let body = serde_json::json!({ "task_id": task_id, "status": status });
2410        let response = self.bare_post(&url, &body.to_string()).await?;
2411        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2412    }
2413
2414    async fn get_share_cloud_base_env(
2415        &self,
2416        appids: &[String],
2417    ) -> Result<GetShareCloudBaseEnvResponse, WxErrorException> {
2418        let svc = self.svc()?;
2419        let config = svc.wx_open_config_storage();
2420        let body = serde_json::json!({ "appids": appids });
2421        let response = self
2422            .post(&urls::batch_get_env_id(config.as_ref()), &body.to_string())
2423            .await?;
2424        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2425    }
2426
2427    async fn get_tcb_env_list(&self) -> Result<GetTcbEnvListResponse, WxErrorException> {
2428        let svc = self.svc()?;
2429        let config = svc.wx_open_config_storage();
2430        let response = self
2431            .post(&urls::describe_envs(config.as_ref()), "{}")
2432            .await?;
2433        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2434    }
2435
2436    async fn change_tcb_env(&self, env: &str) -> Result<WxOpenResult, WxErrorException> {
2437        let svc = self.svc()?;
2438        let config = svc.wx_open_config_storage();
2439        let body = serde_json::json!({ "env": env });
2440        let response = self
2441            .post(&urls::modify_env(config.as_ref()), &body.to_string())
2442            .await?;
2443        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2444    }
2445
2446    async fn share_cloud_base_env(
2447        &self,
2448        request: &ShareCloudBaseEnvRequest,
2449    ) -> Result<ShareCloudBaseEnvResponse, WxErrorException> {
2450        let svc = self.svc()?;
2451        let config = svc.wx_open_config_storage();
2452        let body =
2453            serde_json::to_string(request).map_err(|e| WxErrorException::Serde(e.to_string()))?;
2454        let response = self
2455            .post(&urls::batch_share_env(config.as_ref()), &body)
2456            .await?;
2457        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2458    }
2459
2460    async fn clear_quota_v2(&self, appid: &str) -> Result<WxOpenResult, WxErrorException> {
2461        let svc = self.svc()?;
2462        let config = svc.wx_open_config_storage();
2463        // Java `getWxOpenService().post(...)`(裸 post,无 token 注入)
2464        let body = serde_json::json!({
2465            "appid": appid,
2466            "component_appid": config.component_app_id().unwrap_or_default(),
2467            "appsecret": config.component_app_secret().unwrap_or_default(),
2468        });
2469        let response = self
2470            .bare_post(&urls::clear_quota(config.as_ref()), &body.to_string())
2471            .await?;
2472        WxOpenResult::from_json(&response).map_err(WxErrorException::Serde)
2473    }
2474
2475    async fn apply_set_order_path_info(
2476        &self,
2477        info: &WxOpenMaApplyOrderPathInfo,
2478    ) -> Result<WxOpenResult, WxErrorException> {
2479        let svc = self.svc()?;
2480        let config = svc.wx_open_config_storage();
2481        // Java `new Gson().toJson(info)`;Rust serde 跳过 None 字段(ADAPTED)
2482        let body =
2483            serde_json::to_string(info).map_err(|e| WxErrorException::Serde(e.to_string()))?;
2484        let response = self
2485            .post(&urls::apply_set_order_path_info(config.as_ref()), &body)
2486            .await?;
2487        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2488    }
2489
2490    async fn modify_wxa_server_domain(
2491        &self,
2492        action: &str,
2493        request_domains: &[String],
2494        ws_request_domains: &[String],
2495        upload_domains: &[String],
2496        download_domains: &[String],
2497        udp_domains: &[String],
2498        tcp_domains: &[String],
2499    ) -> Result<WxOpenMaDomainResult, WxErrorException> {
2500        let svc = self.svc()?;
2501        let config = svc.wx_open_config_storage();
2502        let mut body = serde_json::json!({ "action": action });
2503        // Java `!"get".equals(action)` 才带域名数组
2504        if action != "get" {
2505            body["requestdomain"] = serde_json::json!(request_domains);
2506            body["wsrequestdomain"] = serde_json::json!(ws_request_domains);
2507            body["uploaddomain"] = serde_json::json!(upload_domains);
2508            body["downloaddomain"] = serde_json::json!(download_domains);
2509            body["udpdomain"] = serde_json::json!(udp_domains);
2510            body["tcpdomain"] = serde_json::json!(tcp_domains);
2511        }
2512        let response = self
2513            .post(
2514                &urls::modify_wxa_server_domain(config.as_ref()),
2515                &body.to_string(),
2516            )
2517            .await?;
2518        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2519    }
2520
2521    async fn get_domain_confirm_file(
2522        &self,
2523    ) -> Result<WxOpenMaDomainConfirmFileResult, WxErrorException> {
2524        let svc = self.svc()?;
2525        let config = svc.wx_open_config_storage();
2526        let response = self
2527            .post(&urls::get_domain_confirm_file(config.as_ref()), "{}")
2528            .await?;
2529        WxOpenMaDomainConfirmFileResult::from_json(&response).map_err(WxErrorException::Serde)
2530    }
2531
2532    async fn modify_wxa_jump_domain(
2533        &self,
2534        action: &str,
2535        domain_list: &[String],
2536    ) -> Result<String, WxErrorException> {
2537        let svc = self.svc()?;
2538        let config = svc.wx_open_config_storage();
2539        let mut body = serde_json::json!({ "action": action });
2540        if action != "get" {
2541            body["webviewdomain"] = serde_json::json!(domain_list);
2542        }
2543        self.post(
2544            &urls::modify_wxa_jump_domain(config.as_ref()),
2545            &body.to_string(),
2546        )
2547        .await
2548    }
2549
2550    async fn modify_wxa_jump_domain_info(
2551        &self,
2552        action: &str,
2553        domain_list: &[String],
2554    ) -> Result<WxOpenMaWebDomainResult, WxErrorException> {
2555        let response = self.modify_wxa_jump_domain(action, domain_list).await?;
2556        serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))
2557    }
2558}
2559
2560/// spu 增改公共实现(对应 Java `minishopGoodsAddSpu`/`minishopGoodsUpdateSpu`
2561/// 的响应解析差异仅在时间字段键名)。
2562impl WxOpenComponentServiceImpl {
2563    async fn minishop_spu_common<F>(
2564        &self,
2565        app_id: &str,
2566        url_fn: F,
2567        spu: &WxMinishopSpu,
2568        time_key: &str,
2569    ) -> Result<WxMinishopAddGoodsSpuResult, WxErrorException>
2570    where
2571        F: FnOnce(&dyn WxOpenConfigStorage) -> String,
2572    {
2573        let svc = self.svc()?;
2574        let config = svc.wx_open_config_storage();
2575        let url = format!(
2576            "{}?access_token={}",
2577            url_fn(config.as_ref()),
2578            self.get_authorizer_access_token(app_id, true).await?
2579        );
2580        let response = self
2581            .bare_post(&url, &minishop_json::spu(spu).to_string())
2582            .await?;
2583        let resp: serde_json::Value =
2584            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
2585        let mut result = WxMinishopAddGoodsSpuResult::default();
2586        result.errcode = Self::json_i64(&resp, "errcode") as i32;
2587        if result.errcode == 0 {
2588            let data = resp.get("data").cloned().unwrap_or_default();
2589            // Java 以 product_id/out_product_id/时间字段重建 data 对象
2590            let mut data_obj = serde_json::Map::new();
2591            if let Some(pid) = data.get("product_id") {
2592                data_obj.insert("product_id".to_string(), pid.clone());
2593            }
2594            if let Some(oid) = data.get("out_product_id") {
2595                data_obj.insert("out_product_id".to_string(), oid.clone());
2596            }
2597            if let Some(t) = data.get(time_key) {
2598                data_obj.insert(time_key.to_string(), t.clone());
2599            }
2600            result.data = serde_json::Value::Object(data_obj);
2601        } else {
2602            result.errmsg = Self::json_str(&resp, "errmsg");
2603        }
2604        Ok(result)
2605    }
2606
2607    /// sku 新增公共实现(对应 Java `minishiopGoodsAddSku` 响应解析:
2608    /// data{sku_id, create_time})。
2609    async fn minishop_sku_add_common<F>(
2610        &self,
2611        app_id: &str,
2612        url_fn: F,
2613        sku: &WxMinishopSku,
2614    ) -> Result<WxMinishopAddGoodsSpuResult, WxErrorException>
2615    where
2616        F: FnOnce(&dyn WxOpenConfigStorage) -> String,
2617    {
2618        let svc = self.svc()?;
2619        let config = svc.wx_open_config_storage();
2620        let url = format!(
2621            "{}?access_token={}",
2622            url_fn(config.as_ref()),
2623            self.get_authorizer_access_token(app_id, true).await?
2624        );
2625        let response = self
2626            .bare_post(&url, &minishop_json::sku(sku).to_string())
2627            .await?;
2628        let resp: serde_json::Value =
2629            serde_json::from_str(&response).map_err(|e| WxErrorException::Serde(e.to_string()))?;
2630        let mut result = WxMinishopAddGoodsSpuResult::default();
2631        result.errcode = Self::json_i64(&resp, "errcode") as i32;
2632        if result.errcode == 0 {
2633            let data = resp.get("data").cloned().unwrap_or_default();
2634            let mut data_obj = serde_json::Map::new();
2635            if let Some(sid) = data.get("sku_id") {
2636                data_obj.insert("sku_id".to_string(), sid.clone());
2637            }
2638            if let Some(t) = data.get("create_time") {
2639                data_obj.insert("create_time".to_string(), t.clone());
2640            }
2641            result.data = serde_json::Value::Object(data_obj);
2642        } else {
2643            result.errmsg = Self::json_str(&resp, "errmsg");
2644        }
2645        Ok(result)
2646    }
2647}