文档中心开发者技术手册区域本地化支付网关与特色服务

🇮🇷 本地化与区域特色扩展功能开发指南 (Iran Features)

Next-Strapi-Medusa 原生内置了对区域性特殊商业基础设施的企业级支持。当配置环境变量 ENABLE_IRAN_FEATURES=true 时,系统将自动激活手机验证码(SMS OTP)免密登录、本地在线银行卡网关(Zarinpal)、实时运费估算以及智能法币汇率抹零换算,全程无需侵入修改 Medusa 核心代码。

无侵入架构,保障 Medusa 官方平滑升级
所有区域化专用模块(支付网关、短信 OTP 及特种物流)均遵循 Medusa v2 官方标准以独立隔离插件(Plugin)形式实现,确保未来跟随官方大版本迭代时零兼容顾虑。

1. 全局开关 ENABLE_IRAN_FEATURES

要在整个技术栈(Medusa 商业后端、Strapi CMS 及 Next.js 前台)中统一启用区域特色扩展,只需在 .env 与 docker-compose.yml 中配置如下变量:

.env
env
ENABLE_IRAN_FEATURES=true
NEXT_PUBLIC_ENABLE_IRAN_FEATURES=true
DEFAULT_REGION=IR
DEFAULT_LOCALE=fa-IR

2. 手机号码与短信验证码登录认证 (Phone Auth & OTP)

专用手机验证码认证服务实现于 store-admin/src/modules/auth/providers/otp 模块。它自动规范化各制式手机号、生成 4–6 位安全动态验证码并内置频率拦截器。

  • 多格式智能归一化:自动兼容识别各种带国际区号或本地格式的手机号输入。
  • 倒计时冷却与频控:强制实行 120 秒发送冷却周期,杜绝短信轰炸与接口滥用。
  • 免注册静默开户:未注册的新手机号在首次短信核身成功后,系统将自动为其开立新客户档案。
store-admin/medusa-config.ts (phone-auth 手机认证模块配置)
typescript
{
resolve: "./src/modules/auth/providers/otp",
id: "phone-auth",
options: {
jwtSecret: process.env.JWT_SECRET,
},
}

3. 短信平台网关 (Kavenegar 与 SMS.ir)

短信服务底层打通了极速专用通道,支持基于模板变量在 5 秒内将核验码送达用户终端:

.env (短信通道配置参数)
env
# 短信服务商:sms-ir 或 kavenegar
SMS_PROVIDER=sms-ir
SMS_API_KEY=your_sms_api_key
SMS_OTP_TEMPLATE_ID=100000 # 验证码短信模板 ID
SMS_ORDER_PLACED_TEMPLATE_ID=200000 # 订单支付成功确认模板 ID

4. 在线银行卡支付通道 (Zarinpal)

Zarinpal 支付模块深度接入 Medusa 支付会话生命周期,提供具备金融级安全的直联对账,并支持一键开启沙箱沙盒测试(Sandbox):

.env (在线网关参数)
env
ZARINPAL_MERCHANT_ID=your-merchant-id-xxxx-xxxx
ZARINPAL_SANDBOX=false # 本地联调时可设为 true

5. 实时物流费用精准核算 (Fulfillment)

medusa-fulfillment-iran-post 插件在结算页面依据收发货物理网格及商品净重动态调用物流资费接口,并在订单后台完整支持 24 位物流追踪运单号回填。

store-admin/medusa-config.ts (物流履约运费插件配置)
typescript
{
resolve: "./src/modules/fulfillment/providers/iran-post",
id: "iran-post",
options: {
originProvince: "تهران",
originCity: "تهران",
defaultPackagingWeight: 150, // گرم
},
}

6. 智能货币单位抹零换算 (convertToToman)

数据库中始终以最小法币分辅币标准存储。在 Strapi 后台的 Storefront Settings 中勾选 convertToToman 开关后,前端会自动执行抹零并按千位分隔符展示本土习惯的标价。

前端金额本地化换算代码 (store/src/lib/util/prices.ts)
typescript
export function formatPrice(amount: number, currencyCode: string, convertToToman = false) {
if (currencyCode.toLowerCase() === 'irr' && convertToToman) {
const tomanAmount = Math.round(amount / 10);
return `${new Intl.NumberFormat('fa-IR').format(tomanAmount)} تومان`;
}
return new Intl.NumberFormat('fa-IR', { style: 'currency', currency: currencyCode }).format(amount);
}