东南亚连锁团队扩第二座城时,海外版出海外卖平台最常踩的坑不是「再部署一套 App」,而是多城费率、配送范围、商家资质混在同一套全局配置里,A 城改规则 B 城跟着变;用户商家资料若不分城存储,还会出现「曼谷注册的店出现在清迈列表」。
多城扩张若采用「每城一套库、总部登八个后台」的笨办法,运营成本线性上涨。更可行的方向是一套登录、行级数据范围、分城规则表——总部看仪表盘,城站只能改本城商家,用户资料按城隔离但账号可跨国复用。
下文从表结构、数据范围 Service、验收 SQL 说明「多城规则与用户商家资料如何分开放」。示例为教学示意,以光合同城当期海外交付为准。海外支付与税务各国不同,支持按需定制对接;商务结算规则由客户在当地确定。
痛点:一套后台管多城,改规则互相污染
常见反模式:
merchant表无city_code,靠运营手工打标签筛城- 费率、配送半径存在全局表,改 A 城即全量生效
- 用户地址簿不绑城,跨城下单时运费算错
- 总部运营与城站运营共用超管,误改他城商家资料
海外版出海外卖平台若要多城共用一个后台登录,架构上应先定数据行级范围(city scope),再谈 UI 有几个「城市切换」Tab。
组织与城市维度表
CREATETABLEorg_city(city_codeVARCHAR(12)PRIMARYKEY,country_codeVARCHAR(4)NOTNULL,timezoneVARCHAR(32)NOTNULL,currencyVARCHAR(8)NOTNULL,statusVARCHAR(8)NOTNULLDEFAULT'active');CREATETABLEorg_station(idBIGINTPRIMARYKEYAUTO_INCREMENT,city_codeVARCHAR(12)NOTNULL,station_nameVARCHAR(64)NOTNULL,FOREIGNKEY(city_code)REFERENCESorg_city(city_code));城站运营账号绑定station_id,总部账号绑定country_code或全量。Claims 里必须带city_codes数组,网关层过滤查询。
用户资料:账号全局、地址与偏好按城
用户账号可跨国共用(同一手机号登录),但配送地址、常用门店、本地合规同意记录按城分开。
CREATETABLEum_user(idBIGINTPRIMARYKEYAUTO_INCREMENT,mobileVARCHAR(20)NOTNULLUNIQUE,global_statusVARCHAR(8)NOTNULLDEFAULT'active',created_atDATETIMENOTNULL);CREATETABLEum_user_city_profile(user_idBIGINTNOTNULL,city_codeVARCHAR(12)NOTNULL,localeVARCHAR(16)NOTNULLDEFAULT'en-US',consent_versionVARCHAR(16)NULL,last_login_atDATETIMENULL,PRIMARYKEY(user_id,city_code));CREATETABLEum_user_address(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,city_codeVARCHAR(12)NOTNULL,detailVARCHAR(256)NOTNULL,is_defaultTINYINTNOTNULLDEFAULT0,INDEXidx_addr_user_city(user_id,city_code));验收用例:用户在曼谷下单后切到清迈,首页商家列表应只出现city_code=CNX的门店;曼谷地址不应作为清迈默认地址。
商家资料:一店一城,规则按城快照
商家主体可按连锁总部统一,但营业资质、配送范围、费率规则必须带city_code。
CREATETABLEum_merchant(idBIGINTPRIMARYKEYAUTO_INCREMENT,brand_nameVARCHAR(128)NOTNULL,hq_countryVARCHAR(4)NOTNULL);CREATETABLEum_merchant_city(merchant_idBIGINTNOTNULL,city_codeVARCHAR(12)NOTNULL,local_licenseVARCHAR(64)NULL,delivery_radius_kmDECIMAL(6,2)NOTNULL,statusVARCHAR(8)NOTNULLDEFAULT'active',PRIMARYKEY(merchant_id,city_code));多城费率规则引用上一篇思路,在wm_fee_rule增加city_code NOT NULL,禁止 NULL 表示「全城通用」除非总部 explicitly 配置。
数据范围 Service:查询必带 city filter
@DataScope(cities="#claims.cityCodes")publicList<MerchantVO>listMerchants(MerchantQueryq,UserClaimsclaims){// MyBatis 拦截器自动追加 AND city_code IN (...)returnmerchantMapper.selectByQuery(q);}publicvoidupdateMerchantCity(MerchantCityUpdatecmd,UserClaimsclaims){if(!claims.getCityCodes().contains(cmd.getCityCode())){thrownewForbiddenException("CITY_SCOPE_DENIED");}merchantCityRepo.update(cmd);}代码块对应读者要确认的一件事:城站运营改不了他城商家,总部改规则时可选择「仅本城」或「全国连锁模板下发」。若只有菜单权限没有行级 filter,多城迟早串数据。
规则下发与复制:模板不等于全局覆盖
总部可维护rule_template,下发到某城时生成该城独立fee_rule行,而不是所有城共用同一rule_id。
INSERTINTOwm_fee_rule(merchant_id,city_code,rule_type,rate_value,...)SELECTm.merchant_id,:target_city,t.rule_type,t.rate_value,...FROMrule_template tJOINum_merchant_city mONm.city_code=:target_cityWHEREt.template_code=:code;下发后各城可独立微调,互不影响。验收:改清迈某店费率,曼谷同品牌店数值不变。
总部与城站登录:同一入口不同数据范围
多城共后台不等于人人看全城。JWT Claims 示例:
{"user_id":9001,"roles":["city_manager"],"city_codes":["BKK"],"country_code":"TH"}总部角色city_codes可为["*"]或按国家展开;城站角色必须显式列表。Admin 列表页默认带city_code筛选项,且不可被前端参数绕过——服务端强制 append filter。
跨城用户下单:地址与门店范围校验
用户旅行到另一城时,允许浏览该城门店,但支付币种与配送费规则跟当前城走。下单接口应校验:merchant.city_code == address.city_code == pricing.city_code,不一致则拒单并提示切换城市,而不是静默按错误运费出单。
publicvoidvalidateCrossCity(OrderCreateCmdcmd){Stringmc=merchantRepo.getCity(cmd.getMerchantId());Stringac=cmd.getAddress().getCityCode();if(!mc.equals(ac)){thrownewBizException("CROSS_CITY_NOT_ALLOWED");}}合规与展示字段:按国按城配置
海外版小票、发票展示字段各国不同,不应写死在订单 Service。建议compliance_field表按country_code + city_code配置必填项,支持按需定制对接税务相关导出;不默认某一国税制。支付通道同理,各国接口不同,桥接层按城或按国启用。
评审三问(多城资料)
- 改 A 城商家配送半径,B 城同品牌是否不变?
- 城站账号能否通过改参数查看他城订单 ID?
- 用户跨城地址是否会被错误门店接单?
光合同城海外版怎么对应
光合同城海外版支持多城共后台、资料分城存储;用户账号可跨国,地址与商家营业范围按城隔离。费率、配送、合规字段可按国家与城市配置,支付通道各国不同,支持按需定制对接。成品提供多语言与本地化底座,具体国家规则以当期交付范围为准。
技术小结
海外版出海外卖平台扩城时,先核对收city_code是否贯穿用户地址、商家营业、费率规则与 API 查询 filter。一套后台不等于一套全局表;邻居应读懂「多城规则和用户商家资料分开放,改 A 城不动 B 城」。