diff --git a/apps/admin/app/pages/guestaccess.vue b/apps/admin/app/pages/guestaccess.vue index e7621684..f1369825 100644 --- a/apps/admin/app/pages/guestaccess.vue +++ b/apps/admin/app/pages/guestaccess.vue @@ -5,8 +5,8 @@

游客显隐控制

- 控制「游客登录」入口与「实名认证」对哪一版客户端可见——两者同一个开关,都是为应对审核。 - 整个应用一套,不分渠道、不分 Android / iOS。 + 控制「游客登录」入口与「实名认证」是否对客户端可见——两者同一个开关。 + 整个应用、全部版本一套,不分渠道、不分 Android / iOS。

@@ -30,14 +30,10 @@ 允许游客登录(同时开启实名认证) -
- 生效版本(必填) - -

- 填客户端上报的净版本号,不含构建号(1.0.1 而不是 1.0.1+5)。 -

-
-

⚠ {{ warn }}

+

+ 开则全部版本都显示游客登录并要求实名;关则全部隐藏、功能不受实名限制。 + 不能只给送审包开——商店会按「审核时隐藏功能」拒审。 +

@@ -61,30 +57,24 @@
这个开关怎么判定

- 白名单语义:只有填在「生效版本」里的那一版能看到入口,其余版本一律隐藏。 + 全版本一套:开就全部可见,关就全部隐藏。审核员和线上用户必须看到同一套入口。

- + - - - + +
开关生效版本游客登录 + 实名认证
开关游客登录 + 实名认证
关任意全部版本隐藏
开空全部版本隐藏(保存时会被拦下)
开1.0.1只有 1.0.1 可见,其余隐藏
关全部版本隐藏,功能不受实名限制
开全部版本可见,并要求实名才能用功能

- 典型用法是送审:只给正在过审的那一版开游客登录,线上其它版本不受影响。 -

-

- 实名认证跟着这个开关走:能看到游客登录的那一版,同时显示「实名认证」并要求实名才能用功能; - 其余版本实名认证整个隐藏,所有功能不受实名限制。已有的实名记录不受影响。 + 实名认证跟着这个开关走。已有的实名记录不受影响。 本规则之前发布的老版本 App 不认识它,仍一律要求实名。

- 判定在服务端做(客户端只拿结论,不拿规则)。留空按「全隐藏」而非「全放行」处理是刻意选的失败方向—— - 判宽会让不该露入口的版本露出去(过审风险、白嫖开户礼),判严只是入口少显示。 - 要全部隐藏请直接取消勾选,不要靠留空版本号表达。 + 判定在服务端做(客户端只拿结论,不拿规则)。 + 不要再按版本只给送审包开入口——那正是商店 Guideline 5.6 会拒的做法。

客户端拿不到配置(无网/接口失败)时同样按隐藏处理,所以断网启动后短时间内入口不出现是预期行为; @@ -104,7 +94,6 @@ definePageMeta({ title: '游客显隐控制' }) interface GuestForm { tourists: boolean - touristsversion: string } const { webApi, consoleApi } = useApi() @@ -113,47 +102,17 @@ const { success, error } = useToast() const scopeApp = ref('') const scopeApps = ref([]) const submitting = ref(false) -// 线上版本号只读,用来提示「生效版本填得比线上还高 = 目前没人看得到」 -const onlineVersion = ref('') function emptyForm(): GuestForm { - return { tourists: false, touristsversion: '' } + return { tourists: false } } const form = reactive(emptyForm()) -// 与后端 comm.CompareVersion 同语义:切掉构建号、逐段按整数比、短的补 0 -function cmpVer(a: string, b: string): number { - const as = a.split('+')[0]!.trim().split('.') - const bs = b.split('+')[0]!.trim().split('.') - const n = Math.max(as.length, bs.length) - for (let i = 0; i < n; i++) { - const x = parseInt(as[i] ?? '0') || 0 - const y = parseInt(bs[i] ?? '0') || 0 - if (x !== y) return x < y ? -1 : 1 - } - return 0 -} - -// 这里只提示,真正的拦截在后端 saveAppTourists(前端提示挡不住直接调接口) -const warn = computed(() => { - if (!form.tourists) return '' - const v = form.touristsversion.trim() - if (!v) return '开启游客登录必须填生效版本,留空等于所有版本都看不到入口' - if (onlineVersion.value.trim() && cmpVer(v, onlineVersion.value) > 0) { - return `生效版本高于「当前线上版本 ${onlineVersion.value}」,目前线上没有任何一版客户端能看到入口` - } - return '' -}) - const effect = computed(() => { if (!form.tourists) { return { tone: 'off', icon: '🚫', title: '全部版本隐藏', desc: '所有客户端都不显示游客登录入口与实名认证,功能不受实名限制。' } } - const v = form.touristsversion.trim() - if (!v) { - return { tone: 'warn', icon: '⚠️', title: '等于没开', desc: '勾了开关却没点名版本,所有版本仍然看不到入口(实名同样隐藏)。保存时会被拦下。' } - } - return { tone: 'on', icon: '✅', title: `只有 ${v} 可见`, desc: `上报版本号等于 ${v} 的客户端显示游客登录入口与实名认证、并要求实名;其余版本两者都隐藏,功能不受实名限制。` } + return { tone: 'on', icon: '✅', title: '全部版本可见', desc: '所有客户端都显示游客登录入口与实名认证,并要求实名才能用功能。' } }) // 应用名来源:应用中心的应用表 + 部署注册表(兜底老数据);无权读这两张表时改走 api_getmyapps。 @@ -184,28 +143,20 @@ async function load() { const d = await webApi<{ item: GuestForm & { version?: string } }>('getapprelease', { app_name: scopeApp.value }) Object.assign(form, emptyForm(), { tourists: d?.item?.tourists ?? false, - touristsversion: d?.item?.touristsversion ?? '', }) - onlineVersion.value = d?.item?.version ?? '' } catch (e: any) { error(e?.message ?? '加载失败') Object.assign(form, emptyForm()) - onlineVersion.value = '' } } async function save() { - // 关掉开关时顺手清空生效版本:输入框此时是隐藏的,留着旧值会被后端的 - // 「填了版本必须勾选开关」校验挡下,用户看不到是哪一项有问题。 - if (!form.tourists) form.touristsversion = '' - if (warn.value) { error(warn.value); return } submitting.value = true try { // 只提交游客这一组字段:版本控制和它同一行,整行提交会把对方清空。 await webApi('saveapptourists', { app_name: scopeApp.value, tourists: form.tourists, - touristsversion: form.touristsversion, }) success('保存成功') await load() diff --git a/apps/client/ios/Runner.xcodeproj/project.pbxproj b/apps/client/ios/Runner.xcodeproj/project.pbxproj index 09b03595..4ea6cd6e 100644 --- a/apps/client/ios/Runner.xcodeproj/project.pbxproj +++ b/apps/client/ios/Runner.xcodeproj/project.pbxproj @@ -653,7 +653,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 44; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = B8AJQ7ACU7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -847,7 +847,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 44; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = B8AJQ7ACU7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -881,7 +881,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 44; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = B8AJQ7ACU7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/apps/client/lib/core/utils/doc_url_helper.dart b/apps/client/lib/core/utils/doc_url_helper.dart index fab2b80f..2758c41c 100644 --- a/apps/client/lib/core/utils/doc_url_helper.dart +++ b/apps/client/lib/core/utils/doc_url_helper.dart @@ -22,15 +22,10 @@ String docLangPath() { /// 业务服务器上的法律文档目录前缀(按 docLangPath() 完整语言命中) String _legalDocBase() => '${DioManager().baseUrl}/docs/legal/${docLangPath()}'; -/// 会员协议仅有 zh_cn / zh_hk / zh_tw / en 四种文件,其余语言一律回退英文 +/// 会员协议托管中英两份,与隐私政策同一目录。中文一律走 zh_cn,其余语言走 en。 String _memberAgreementLangDir() { final lang = Get.locale?.languageCode ?? 'en'; - if (lang == 'zh') { - final country = Get.locale?.countryCode?.toUpperCase() ?? ''; - if (country == 'CN') return 'zh_cn'; - if (country == 'HK') return 'zh_hk'; - return 'zh_tw'; - } + if (lang == 'zh') return 'zh_cn'; return 'en'; } diff --git a/apps/client/lib/core/utils/id_verify_guard.dart b/apps/client/lib/core/utils/id_verify_guard.dart index 41ea49b0..185fe5d3 100644 --- a/apps/client/lib/core/utils/id_verify_guard.dart +++ b/apps/client/lib/core/utils/id_verify_guard.dart @@ -13,9 +13,9 @@ import '../../modules/common/widgets/id_verify_dialog.dart'; /// 游客登录已改走服务端(拿真实账号与 JWT),实名信息有账号可落, /// 服务端又按设备找回同一个游客账号,所以认证一次就一直有效。 /// -/// **整个闸门受「实名认证显隐」控制**(2026-09-23):与游客登录同一个口子,由服务端按本机版本 -/// 算好下发([VersionUpdateService.showIdVerify])。隐藏时实名入口、「未认证」角标全部不显示, -/// 所有功能不受实名限制;只有能看到游客登录的那一版(送审版本)才显示并拦截。 +/// **整个闸门受「实名认证显隐」控制**:与游客登录同一个口子,由服务端按应用开关 +/// 算好下发([VersionUpdateService.showIdVerify]),全版本同一套结论。 +/// 隐藏时实名入口、「未认证」角标全部不显示,所有功能不受实名限制。 /// 拿不到配置时按隐藏处理,即放行。 class IdVerifyGuard { IdVerifyGuard._(); diff --git a/apps/client/lib/data/models/goods.dart b/apps/client/lib/data/models/goods.dart index f0cb6812..4e9b784b 100644 --- a/apps/client/lib/data/models/goods.dart +++ b/apps/client/lib/data/models/goods.dart @@ -256,6 +256,29 @@ class DBGoods { final key3 = tag.replaceAll('-', '_'); dynamic e = m[key1] ?? m[key2] ?? m[key3]; if (e == null && m.isNotEmpty) e = m.values.first; - return e is Map ? e as Map : const {}; + return e is Map ? e : const {}; + } + + /// 按界面语言取这一档的名称、货币和价格。 + /// 中文用 zh-*(人民币),英文以及没单独配的语言用 en-*(美元)。 + Map localeForLanguage(String languageCode) { + final m = _localnameMap; + if (m == null || m.isEmpty) return const {}; + final lang = languageCode.toLowerCase(); + Map? sameLanguage; + for (final e in m.entries) { + if (e.value is! Map) continue; + final key = e.key.toLowerCase().replaceAll('_', '-'); + if (key.split('-').first != lang) continue; + final row = Map.from(e.value as Map); + final region = key.contains('-') ? key.split('-').last : ''; + if (lang == 'zh' && region == 'cn') return row; + if (lang == 'en' && region == 'us') return row; + sameLanguage ??= row; + } + if (sameLanguage != null) return sameLanguage; + if (lang != 'en') return localeForLanguage('en'); + final first = m.values.first; + return first is Map ? Map.from(first) : const {}; } } diff --git a/apps/client/lib/data/services/version_update_service.dart b/apps/client/lib/data/services/version_update_service.dart index 66898630..679d4708 100644 --- a/apps/client/lib/data/services/version_update_service.dart +++ b/apps/client/lib/data/services/version_update_service.dart @@ -28,10 +28,10 @@ class VersionUpdateService extends GetxService { static AppInfo? _appInfo; //应用信息 static bool? _isShowTourists = false; //是否显示游客登录 - /// 这一版是否显示实名认证并按实名限制功能(服务端结论,与游客登录同一个口子)。 + /// 是否显示实名认证并按实名限制功能(服务端结论,与游客登录同一个口子,全版本一套)。 /// /// 做成 Rx:「未认证」角标、「我的」页实名行在 Obx 里读它,结论到达/变化时要能跟着显隐。 - /// 拿不到配置(断网、接口失败、渠道没配)一律 false = 隐藏实名、放行功能(2026-09-23 定的失败方向)。 + /// 拿不到配置(断网、接口失败、渠道没配)一律 false = 隐藏实名、放行功能。 static final RxBool showIdVerify = false.obs; static bool? _allowSkipDeviceBinding = false; //是否允许跳过设备绑定 static DateTime? _lastCheckTime; //上次检查时间 diff --git a/apps/client/lib/modules/goods/utils/goods_i18n.dart b/apps/client/lib/modules/goods/utils/goods_i18n.dart index 7900ce24..96d318c0 100644 --- a/apps/client/lib/modules/goods/utils/goods_i18n.dart +++ b/apps/client/lib/modules/goods/utils/goods_i18n.dart @@ -1,17 +1,10 @@ import 'package:get/get.dart'; import '../../../data/models/goods.dart'; -String _localeTag() { - final l = Get.locale; - if (l == null) return 'zh-CN'; - final cc = l.countryCode; - return cc != null && cc.isNotEmpty - ? '${l.languageCode}-${cc.toUpperCase()}' - : l.languageCode; -} +String _uiLanguage() => Get.locale?.languageCode ?? 'zh'; String localizedGoodsName(DBGoods item) { - final info = item.localInfo(_localeTag()); + final info = item.localeForLanguage(_uiLanguage()); final ln = (info['name'] as String?)?.trim() ?? ''; if (ln.isNotEmpty) return ln; final id = item.id.trim(); @@ -67,16 +60,54 @@ String localizedGoodsDesc(DBGoods item) { } } +String _trimAmount(double v) { + final fixed = v.toStringAsFixed(2); + return fixed.replaceFirst(RegExp(r'\.?0+$'), ''); +} + String goodsDisplayPrice(DBGoods item) { - final info = item.localInfo(_localeTag()); + final lang = _uiLanguage(); + final info = item.localeForLanguage(lang); final cur = (info['currency'] as String?)?.trim(); final p = info['price']; - final symbol = (cur != null && cur.isNotEmpty) ? cur : '¥'; + final symbol = (cur != null && cur.isNotEmpty) + ? cur + : (lang == 'zh' ? '¥' : r'$'); double v; if (p is num) { v = p.toDouble(); } else { v = item.priceYuan; } - return '$symbol${v.toStringAsFixed(2)}'; + return '$symbol${_trimAmount(v)}'; +} + +/// 这一档自己的服务说明:天数和算力都取商品本身,不跟账号当前状态混在一起。 +/// 一年及以上按终生,显示无限。算力为 0 也照实写,避免三档看起来发了同一份。 +List vipOfferFacts(DBGoods item) { + final zh = _uiLanguage() == 'zh'; + final lines = []; + final days = item.viptime; + if (days >= 365) { + lines.add(zh ? '到期时间:无限' : 'Expires: unlimited'); + } else if (days > 0) { + lines.add(zh ? '到期时间:$days 天' : 'Expires: $days days'); + } + lines.add(zh ? '算力:${item.compute}' : 'Compute: ${item.compute}'); + return lines; +} + +/// 会员页三档标题。中文是人民币,其它语言用美元价。 +String vipOfferTitle(String tier, String price) { + final zh = _uiLanguage() == 'zh'; + switch (tier) { + case 'month': + return zh ? '月度$price VIP充值' : 'Monthly $price VIP'; + case 'quarter': + return zh ? '季度$price VIP充值' : 'Quarterly $price VIP'; + case 'lifetime': + return zh ? '终生$price VIP充值' : 'Lifetime $price VIP'; + default: + return price; + } } diff --git a/apps/client/lib/modules/goods/views/goodsvip_view.dart b/apps/client/lib/modules/goods/views/goodsvip_view.dart index f8b3e6dc..eaa04f7b 100644 --- a/apps/client/lib/modules/goods/views/goodsvip_view.dart +++ b/apps/client/lib/modules/goods/views/goodsvip_view.dart @@ -51,14 +51,15 @@ class GoodsvipView extends GetView { final focused = _tier != null; final source = focused ? controller.items.where(_matchesTier).toList() - : controller.items.toList(); + : controller.items + .where((g) => g.usagetype == UsageType.Vip) + .toList(); final Map> grouped = {}; for (final g in source) { grouped.putIfAbsent(g.usagetype, () => []).add(g); } - // 从「我的」点某一档进来时只留会员服务和这一档;其它入口仍是完整充值页。 - final order = - focused ? [UsageType.Vip] : [UsageType.Vip, UsageType.Compute]; + // 会员页只放月 / 季 / 终生三档。点某一档进来时只留那一档。 + final order = [UsageType.Vip]; final children = []; if (!focused) { if (controller.vipExpiringSoon) { @@ -73,8 +74,6 @@ class GoodsvipView extends GetView { children.add(_buildWarnBanner(isDarkMode, 'computeLow'.tr)); children.add(SizedBox(height: 12.h)); } - children.add(_buildFeaturesSection(isDarkMode)); - children.add(SizedBox(height: 16.h)); } for (int i = 0; i < order.length; i++) { final t = order[i]; @@ -132,7 +131,7 @@ class GoodsvipView extends GetView { case 'lifetime': return 'vipLifetime'.tr; default: - return 'aiRecharge'.tr; + return 'vipRecharge'.tr; } } @@ -145,7 +144,7 @@ class GoodsvipView extends GetView { case 'month': return g.viptime >= 28 && g.viptime <= 31; case 'quarter': - return g.viptime >= 80 && g.viptime <= 100; + return g.viptime >= 70 && g.viptime <= 120; case 'lifetime': return g.viptime >= 365; } @@ -196,133 +195,6 @@ class GoodsvipView extends GetView { ); } - Widget _buildFeaturesSection(bool isDarkMode) { - final textColor = isDarkMode ? Colors.white : Colors.black; - final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'featureDescription'.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: textColor, - ), - ), - SizedBox(height: 10.h), - Wrap( - spacing: 12.w, - runSpacing: 12.h, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - children: [ - _buildFeatureCard( - isDarkMode, - icon: Icons.workspace_premium, - title: 'benefitsOverview'.tr, - lines: ['higherQuota'.tr, 'lowerUnitPrice'.tr], - colorSeed: Colors.amber, - ), - _buildFeatureCard( - isDarkMode, - icon: Icons.support_agent, - title: 'priorityService'.tr, - lines: ['exclusiveSupportChannel'.tr, 'fastResponse'.tr], - colorSeed: Colors.purple, - ), - _buildFeatureCard( - isDarkMode, - icon: Icons.local_offer, - title: 'eventsAndDiscounts'.tr, - lines: ['memberExclusiveEvents'.tr, 'longTermDiscountPlan'.tr], - colorSeed: Colors.indigo, - ), - _buildFeatureCard( - isDarkMode, - icon: Icons.auto_awesome, - title: 'generalPrivileges'.tr, - lines: ['earlyAccessNewFeatures'.tr, 'crossDeviceSync'.tr], - colorSeed: Colors.teal, - ), - ], - ), - SizedBox(height: 12.h), - Text( - 'vipDisclaimer'.tr, - style: TextStyle(fontSize: 12.sp, color: subColor), - ), - ], - ); - } - - Widget _buildFeatureCard( - bool isDarkMode, { - required IconData icon, - required String title, - required List lines, - required MaterialColor colorSeed, - }) { - final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; - final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; - final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; - final width = (Get.width - 16.w * 2 - 12.w) / 2; - return Container( - width: width, - padding: EdgeInsets.all(12.w), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 36.w, - height: 36.w, - decoration: BoxDecoration( - color: isDarkMode - ? mainColor.withOpacity(0.25) - : mainColor.withOpacity(0.15), - borderRadius: BorderRadius.circular(18.r), - ), - child: Icon(icon, color: mainColor), - ), - SizedBox(width: 8.w), - Expanded( - child: Text( - title, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - ], - ), - SizedBox(height: 8.h), - ...lines.map((l) => Padding( - padding: EdgeInsets.only(bottom: 4.h), - child: - Text(l, style: TextStyle(fontSize: 12.sp, color: subColor)), - )), - ], - ), - ); - } - Widget _buildTypeHeader(UsageType type, bool isDarkMode) { final titles = { UsageType.AI: 'aiFunction'.tr, @@ -339,21 +211,7 @@ class GoodsvipView extends GetView { vipSubtitle = controller.computeSummary; } if (type == UsageType.Vip) { - int vipexptime = 0; - if (User.isLoggedIn()) { - final u = User.instance; - vipexptime = u.vipexptime; - } - DateTime? expiry; - if (vipexptime > 0) { - expiry = vipexptime > 1000000000000 - ? DateTime.fromMillisecondsSinceEpoch(vipexptime) - : DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); - } - // titleStr = 'Vip $viplv'; - vipSubtitle = expiry != null - ? '${'expiryLabel'.tr}${_fmtDate(expiry)}${controller.vipFromDevice ? ' · ${'vipFromDevice'.tr}' : ''}' - : 'notActivated'.tr; + vipSubtitle = _vipExpiryLine(withDeviceSource: true); } return Container( padding: EdgeInsets.all(16.w), @@ -421,6 +279,10 @@ class GoodsvipView extends GetView { Widget _buildTypeSection( UsageType type, List list, bool isDarkMode) { + // 三档同页时,到期和算力写在各自的卡片里,不再在上面放一条共用的会员说明。 + if (type == UsageType.Vip && _tier == null) { + return _buildGoodsGrid(list, type, isDarkMode); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -436,17 +298,8 @@ class GoodsvipView extends GetView { final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; if (type == UsageType.Vip) { int viplv = 0; - int vipexptime = 0; if (User.isLoggedIn()) { - final u = User.instance; - viplv = u.viplv; - vipexptime = u.vipexptime; - } - DateTime? expiry; - if (vipexptime > 0) { - expiry = vipexptime > 1000000000000 - ? DateTime.fromMillisecondsSinceEpoch(vipexptime) - : DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); + viplv = User.instance.viplv; } return Row( children: [ @@ -464,9 +317,7 @@ class GoodsvipView extends GetView { ), SizedBox(height: 4.h), Text( - expiry != null - ? '${'validityPeriod'.tr}${_fmtDate(expiry)}' - : 'notBound'.tr, + _vipExpiryLine(labelKey: 'validityPeriod'), style: TextStyle(fontSize: 12.sp, color: subColor), ), ], @@ -531,7 +382,47 @@ class GoodsvipView extends GetView { ); } + String? _tierKind(DBGoods g) { + if (g.usagetype != UsageType.Vip) return null; + if (g.viptime > 0) { + if (g.viptime >= 28 && g.viptime <= 31) return 'month'; + if (g.viptime >= 70 && g.viptime <= 120) return 'quarter'; + if (g.viptime >= 365) return 'lifetime'; + } + final blob = '${g.id} ${g.name} ${g.localname}'.toLowerCase(); + if (blob.contains('month') || blob.contains('月度')) return 'month'; + if (blob.contains('quarter') || blob.contains('季')) return 'quarter'; + if (blob.contains('life') || blob.contains('终')) return 'lifetime'; + return null; + } + Widget _buildGoodsGrid(List list, UsageType type, bool isDarkMode) { + if (type == UsageType.Vip && _tier == null) { + final ordered = [...list]..sort((a, b) { + int rank(DBGoods g) { + switch (_tierKind(g)) { + case 'month': + return 0; + case 'quarter': + return 1; + case 'lifetime': + return 2; + default: + return 3; + } + } + + return rank(a).compareTo(rank(b)); + }); + return Column( + children: [ + for (final item in ordered) ...[ + _buildVipOfferCard(item, isDarkMode), + SizedBox(height: 12.h), + ], + ], + ); + } return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -546,6 +437,81 @@ class GoodsvipView extends GetView { ); } + Widget _buildVipOfferCard(DBGoods item, bool isDarkMode) { + final kind = _tierKind(item) ?? ''; + final price = goodsDisplayPrice(item); + final title = vipOfferTitle(kind, price); + final zh = (Get.locale?.languageCode ?? 'zh') == 'zh'; + final facts = vipOfferFacts(item); + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Container( + width: double.infinity, + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w700, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 10.h), + Text( + zh ? '会员服务说明' : 'Membership details', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 6.h), + for (final line in facts) + Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: Text( + line, + style: TextStyle(fontSize: 14.sp, height: 1.4, color: subColor), + ), + ), + SizedBox(height: 12.h), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: () => controller.purchase(item), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + 'purchase'.tr, + style: TextStyle(fontSize: 13.sp, color: Colors.white), + ), + ), + ), + ), + ], + ), + ); + } + Widget _buildGridItemCard(DBGoods item, UsageType type, bool isDarkMode) { final iconColors = _iconColorForType(type, isDarkMode); return Container( @@ -630,6 +596,55 @@ class GoodsvipView extends GetView { ); } + DateTime? _parseVipExpiry() { + if (!User.isLoggedIn()) return null; + var ts = User.instance.vipexptime; + final computeVip = controller.compute.value?['vip']; + if (computeVip is Map) { + final exp = (computeVip['exptime'] as num?)?.toInt() ?? 0; + if (exp > 0) ts = exp; + } + if (ts <= 0) return null; + final expiry = ts > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(ts) + : DateTime.fromMillisecondsSinceEpoch(ts * 1000); + if (!expiry.isAfter(DateTime.now())) return null; + return expiry; + } + + /// 当前有效 VIP 按剩余天数分档:月度约 30 天,季度约 90 天,一年及以上当终生。 + String? _activeVipTier(DateTime expiry) { + final days = expiry.difference(DateTime.now()).inDays; + if (days >= 300) return 'lifetime'; + if (days >= 60) return 'quarter'; + if (days > 0) return 'month'; + return null; + } + + /// 月度 / 季度页只显示对应档位的到期日,避免三条入口共用一条 vipexptime。 + /// 终生不展示具体日期,固定为无限。 + String _vipExpiryLine({ + String labelKey = 'expiryLabel', + bool withDeviceSource = false, + }) { + final suffix = withDeviceSource && controller.vipFromDevice + ? ' · ${'vipFromDevice'.tr}' + : ''; + if (_tier == 'lifetime') { + return '${labelKey.tr}${'unlimited'.tr}$suffix'; + } + final expiry = _parseVipExpiry(); + if (expiry == null) return 'notActivated'.tr; + final kind = _activeVipTier(expiry); + if (kind == 'lifetime') { + return '${labelKey.tr}${'unlimited'.tr}$suffix'; + } + if (_tier != null && kind != _tier) { + return 'notActivated'.tr; + } + return '${labelKey.tr}${_fmtDate(expiry)}$suffix'; + } + String _fmtDate(DateTime d) { final y = d.year.toString().padLeft(4, '0'); final m = d.month.toString().padLeft(2, '0'); diff --git a/apps/client/lib/modules/profile/views/profile_view.dart b/apps/client/lib/modules/profile/views/profile_view.dart index a9a3bf31..4181960c 100644 --- a/apps/client/lib/modules/profile/views/profile_view.dart +++ b/apps/client/lib/modules/profile/views/profile_view.dart @@ -974,6 +974,29 @@ class EditProfileView extends GetView { }, isDarkMode: isDarkMode, ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 会员服务协议 + _buildSimpleNavigationSetting( + title: 'memberServiceAgreementTitle'.tr, + icon: Icons.workspace_premium, + iconBgColor: isDarkMode + ? Colors.purple[900]!.withOpacity(0.3) + : Colors.purple[50]!, + iconColor: + isDarkMode ? Colors.purple[300]! : Colors.purple[600]!, + titleColor: isDarkMode ? Colors.white : Colors.black87, + onTap: () { + Get.to(() => WebViewPage( + url: memberServiceAgreementUrl(), + title: 'memberServiceAgreementTitle'.tr, + )); + }, + isDarkMode: isDarkMode, + ), Divider( height: 1, color: isDarkMode diff --git a/apps/client/lib/modules/settings/views/settings_view.dart b/apps/client/lib/modules/settings/views/settings_view.dart index 5ca6b4a7..ab2139d3 100644 --- a/apps/client/lib/modules/settings/views/settings_view.dart +++ b/apps/client/lib/modules/settings/views/settings_view.dart @@ -237,13 +237,13 @@ class SettingsView extends GetView { // 构建Vip购买卡片 /// 会员充值,紧跟用户信息卡下方。 /// - /// 安卓仍由 [AppFeatures.rechargeEnabled] 控制。iOS 始终展示, - /// 月 / 季 / 终身都进现有的会员商品页,支付走 Apple IAP。 + /// 安卓仍由 [AppFeatures.rechargeEnabled] 控制。iOS 始终展示。 + /// 这里只留一个入口,月 / 季 / 终身在会员页里选。 Widget _buildVipPurchaseCard(bool isDarkMode) { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return _buildIosVipRechargeCard(isDarkMode); + if (defaultTargetPlatform != TargetPlatform.iOS && + !AppFeatures.rechargeEnabled) { + return const SizedBox.shrink(); } - if (!AppFeatures.rechargeEnabled) return const SizedBox.shrink(); return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => Get.toNamed(Routes.goodsvip), @@ -310,108 +310,6 @@ class SettingsView extends GetView { ); } - /// iOS 会员充值:月 / 季 / 终身,都打开现有会员商品页。 - Widget _buildIosVipRechargeCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.fromLTRB(16.w, 14.h, 16.w, 12.h), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: isDarkMode - ? [ - const Color(0xFF0D47A1).withValues(alpha: 0.5), - const Color(0xFF1976D2).withValues(alpha: 0.5), - ] - : [const Color(0xFFE3F2FD), const Color(0xFFBBDEFB)], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - borderRadius: BorderRadius.circular(14.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'vipRecharge'.tr, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w700, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(height: 12.h), - Row( - children: [ - _iosVipItem( - icon: Icons.workspace_premium, - label: 'vipMonth'.tr, - isDarkMode: isDarkMode, - arguments: const {'tier': 'month'}, - ), - _iosVipItem( - icon: Icons.calendar_view_month, - label: 'vipQuarter'.tr, - isDarkMode: isDarkMode, - arguments: const {'tier': 'quarter'}, - ), - _iosVipItem( - icon: Icons.event_available, - label: 'vipLifetime'.tr, - isDarkMode: isDarkMode, - arguments: const {'tier': 'lifetime'}, - ), - ], - ), - ], - ), - ); - } - - Widget _iosVipItem({ - required IconData icon, - required String label, - required bool isDarkMode, - Object? arguments, - }) { - return Expanded( - child: InkWell( - onTap: () => Get.toNamed(Routes.goodsvip, arguments: arguments), - borderRadius: BorderRadius.circular(12.r), - child: Column( - children: [ - Container( - width: 42.w, - height: 42.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.amber[900]!.withValues(alpha: 0.3) - : Colors.white, - shape: BoxShape.circle, - ), - child: Icon( - icon, - size: 20.sp, - color: isDarkMode ? Colors.amber[300]! : Colors.amber[700]!, - ), - ), - SizedBox(height: 6.h), - Text( - label, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : const Color(0xFF111827), - height: 1.2, - ), - ), - ], - ), - ), - ); - } - Widget _buildSystemSettingsSection(bool isDarkMode) { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/apps/client/pubspec.yaml b/apps/client/pubspec.yaml index ab7393ba..558d54b4 100644 --- a/apps/client/pubspec.yaml +++ b/apps/client/pubspec.yaml @@ -1,7 +1,7 @@ name: eaimar description: "Eaimar - AI Voice Assistant." publish_to: "none" -version: 1.0.3+44 +version: 1.0.3+46 environment: sdk: ">=3.3.0 <4.0.0" diff --git a/apps/services/comm/apprelease.go b/apps/services/comm/apprelease.go index a172d529..0de83780 100644 --- a/apps/services/comm/apprelease.go +++ b/apps/services/comm/apprelease.go @@ -134,42 +134,27 @@ func (r *AppRelease) IsReviewing(clientVersion string) bool { return CompareVersion(clientVersion, r.ReviewVersion) == 0 } -// ShowTourists 判断某客户端版本是否该看到「游客登录」入口。 +// ShowTourists 判断客户端是否该看到「游客登录」入口。 // -// 判定一律在服务端做:客户端只拿结论,不拿规则。各端各写一遍版本比较必然出现实现不一致, -// 而这个开关直接关系到应用商店过审,判错代价很高。 +// 判定一律在服务端做:客户端只拿结论,不拿规则。 // -// 语义是「白名单」:**只有被点名的那一个版本可见,其余一律隐藏**。 -// - Tourists=false → 一律隐藏(总开关关掉) -// - Tourists=true 且 TouristsVersion="" → 一律隐藏(没点名任何版本 = 谁都不放行) -// - Tourists=true 且 TouristsVersion=X → 只有版本号等于 X 的客户端可见 +// 全版本一套,不再按版本白名单:审核员和线上用户必须看到同一套入口。 +// - Tourists=false → 全部版本隐藏 +// - Tourists=true → 全部版本可见 // -// 送审场景:只给正在审核的那一版开游客登录,线上其它版本不受影响。 -// -// ⚠️ 留空按「全隐藏」而非「全放行」处理,是刻意选的失败方向:这个开关一旦判宽, -// 后果是不该露出游客入口的版本露了出去(过审风险、白嫖开户礼);判严只是入口少显示。 -// 后台保存时会拦住「勾了开关却没填版本」这种等于没开的配置,避免运营踩空。 -// -// 客户端没上报版本时同样隐藏——点名了版本却认不出来是哪一版,只能从严。 +// TouristsVersion 只作历史字段保留,不再参与判定。按版本只给送审包开入口, +// 会被商店判定为「审核时隐藏功能」(Guideline 5.6)。 func (r *AppRelease) ShowTourists(clientVersion string) bool { if r == nil || !r.Tourists { return false } - want := strings.TrimSpace(r.TouristsVersion) - if want == "" { - return false - } - if strings.TrimSpace(clientVersion) == "" { - return false - } - return CompareVersion(clientVersion, want) == 0 + return true } -// ShowIdVerify 判断某客户端版本是否显示「实名认证」、并按实名限制功能(2026-09-23 定的口径)。 +// ShowIdVerify 判断是否显示「实名认证」、并按实名限制功能。 // -// 与游客登录**同一个口子**:两者都是为应对应用商店审核,所以后台只有「游客显隐控制」一个开关, -// 能看到游客登录的那一版同时显示实名认证并要求实名,其余版本实名整个隐藏、功能不受实名限制。 -// 不分 Android / iOS(用户定的)。 +// 与游客登录同一个口子、全版本一套:开则全部版本显示并要求实名,关则全部隐藏、 +// 功能不受实名限制。不再按送审版本单独打开。 // // 单独成函数、单独下发字段(DBChannelApp.idverify)而不是让客户端直接读 tourists: // 将来要把实名和游客拆开控制时只改这里和后台,不用再发一版 App。 diff --git a/apps/services/comm/apprelease_test.go b/apps/services/comm/apprelease_test.go index f6bbd3c6..2fa0c76d 100644 --- a/apps/services/comm/apprelease_test.go +++ b/apps/services/comm/apprelease_test.go @@ -64,37 +64,26 @@ func TestAppReleaseIsReviewing(t *testing.T) { } } -// 游客登录的版本限定:这是应用商店过审期的开关,判错要么违规放行、要么审核员看不到入口被拒, -// 三段语义各钉一条。 +// 游客登录全版本一套:开就全开、关就全关,不再按版本白名单。 func TestAppReleaseShowTourists(t *testing.T) { - // 总开关关掉:无论版本、无论有没有配生效版本,一律隐藏。 off := &AppRelease{Tourists: false, TouristsVersion: "1.0.2"} - if off.ShowTourists("1.0.2") { - t.Fatal("总开关关掉时不该显示游客登录") + for _, v := range []string{"1.0.2", "9.9.9", ""} { + if off.ShowTourists(v) { + t.Fatalf("总开关关掉时 %q 不该可见", v) + } } - // 开关开、但没点名任何版本:一律隐藏。白名单语义,不是「没限定就全放行」—— - // 判宽的后果是不该露游客入口的版本露了出去,判严只是入口少显示。 - none := &AppRelease{Tourists: true} + on := &AppRelease{Tourists: true} for _, v := range []string{"1.0.2", "9.9.9", ""} { - if none.ShowTourists(v) { - t.Fatalf("未填生效版本时 %q 不该可见", v) + if !on.ShowTourists(v) { + t.Fatalf("总开关打开时 %q 应可见", v) } } - // 开关开、点名 1.0.2:只有这一版可见,高于/低于/没上报一律隐藏。 - only := &AppRelease{Tourists: true, TouristsVersion: "1.0.2"} - if !only.ShowTourists("1.0.2") { - t.Fatal("被点名的版本应可见") - } - // "1.0.2" == "1.0.2.0":段数不同按 0 补齐,别因为后台填了三段、客户端报四段就判不匹配。 - if !only.ShowTourists("1.0.2.0") { - t.Fatal("补零等价的版本应可见") - } - for _, v := range []string{"1.0.1", "1.0.3", "1.0.20", "2.0.0", ""} { - if only.ShowTourists(v) { - t.Fatalf("点名 1.0.2 时 %q 不该可见", v) - } + // 历史字段 TouristsVersion 不再参与判定:开着时即便只填了旧白名单,也必须全版本可见。 + legacy := &AppRelease{Tourists: true, TouristsVersion: "1.0.2"} + if !legacy.ShowTourists("1.0.3") || !legacy.ShowTourists("1.0.2") { + t.Fatal("打开后不得再按 TouristsVersion 只给某一版看") } var nilRel *AppRelease diff --git a/apps/services/modules/console/api_apprelease.go b/apps/services/modules/console/api_apprelease.go index b7c51262..a4a13515 100644 --- a/apps/services/modules/console/api_apprelease.go +++ b/apps/services/modules/console/api_apprelease.go @@ -141,20 +141,6 @@ func (this *serverComp) saveAppTourists(c *gin.Context) { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } - req.TouristsVersion = strings.TrimSpace(req.TouristsVersion) - - // 白名单语义:只有被点名的那一版可见。勾了开关却不填版本 = 谁都看不到,等于没开 —— - // 这是配置期就能发现的错,别让运营以为「已经开了」再回头去查客户端。 - // 要全部隐藏就直接取消勾选,不要靠留空版本号来表达。 - if req.Tourists && req.TouristsVersion == "" { - writeErr(c, pb.ErrorCode_ReqParameterError, "开启游客登录时必须填「生效版本」——留空等于所有版本都看不到入口;要全部隐藏请直接取消勾选") - return - } - if req.TouristsVersion != "" && !req.Tourists { - writeErr(c, pb.ErrorCode_ReqParameterError, "填了「游客登录生效版本」就必须同时勾选「允许游客登录」,否则该版本仍然看不到入口") - return - } - m, err := this.loadAppRelease(req.AppName) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) @@ -162,6 +148,7 @@ func (this *serverComp) saveAppTourists(c *gin.Context) { } m.AppName = req.AppName m.Tourists = req.Tourists - m.TouristsVersion = req.TouristsVersion + // 全版本一套,不再按版本白名单。历史列清空,避免旧包/旧后台误读。 + m.TouristsVersion = "" this.saveAppRelease(c, m) } diff --git a/apps/services/modules/user/module_test.go b/apps/services/modules/user/module_test.go index 8c63f503..f799621a 100644 --- a/apps/services/modules/user/module_test.go +++ b/apps/services/modules/user/module_test.go @@ -1062,13 +1062,16 @@ func TestCollationCharset(t *testing.T) { } } -// 实名认证的显隐随游客走:点名的那一版下发 idverify=true,其余版本 false(客户端据此整个放行实名闸门)。 +// 实名认证的显隐随游客走,且全版本同一套结论。 func TestToPbChannelAppIdVerifyFollowsTourists(t *testing.T) { - rel := &comm.AppRelease{Version: "1.0.3", Tourists: true, TouristsVersion: "1.0.3"} - if got := toPbChannelApp(comm.ChannelGlobalApple, nil, rel, "1.0.3", ""); !got.Idverify || !got.Tourists { - t.Fatalf("送审版本应同时显示游客与实名,实际 tourists=%v idverify=%v", got.Tourists, got.Idverify) + on := &comm.AppRelease{Version: "1.0.3", Tourists: true, TouristsVersion: "1.0.3"} + for _, v := range []string{"1.0.2", "1.0.3"} { + if got := toPbChannelApp(comm.ChannelGlobalApple, nil, on, v, ""); !got.Idverify || !got.Tourists { + t.Fatalf("开关打开时 %s 应同时显示游客与实名,实际 tourists=%v idverify=%v", v, got.Tourists, got.Idverify) + } } - if got := toPbChannelApp(comm.ChannelChinaXiaomi, nil, rel, "1.0.2", ""); got.Idverify || got.Tourists { - t.Fatalf("非送审版本应同时隐藏游客与实名,实际 tourists=%v idverify=%v", got.Tourists, got.Idverify) + off := &comm.AppRelease{Version: "1.0.3", Tourists: false, TouristsVersion: "1.0.3"} + if got := toPbChannelApp(comm.ChannelChinaXiaomi, nil, off, "1.0.3", ""); got.Idverify || got.Tourists { + t.Fatalf("开关关闭时应同时隐藏游客与实名,实际 tourists=%v idverify=%v", got.Tourists, got.Idverify) } } diff --git a/docs/eaimar/member_service_agreement_en.html b/docs/eaimar/member_service_agreement_en.html new file mode 100644 index 00000000..8d241735 --- /dev/null +++ b/docs/eaimar/member_service_agreement_en.html @@ -0,0 +1,69 @@ + + + + + + Membership Service Agreement - EAIMAR + + + +

Membership Service Agreement

+

EAIMAR Mobile Application
Last updated: October 24, 2026
Effective date: October 24, 2026

+

Welcome to the membership services provided by EAIMAR (the "App"). Before purchasing, subscribing to, or using any membership service, please carefully read and fully understand this Agreement, in particular the clauses regarding fees, automatic renewal, refunds, limitations of liability, and dispute resolution. By tapping Agree, completing payment, or otherwise accepting the membership service, you are deemed to have read and agreed to be bound by this Agreement.

+

If you do not agree to this Agreement, please do not purchase or use the membership service.

+ +

1. Definitions

+
    +
  1. Operator means Shenzhen Yimai Intelligent Technology Co., Ltd., the developer and operator of the App and the membership services.
  2. +
  3. Membership service means the paid member benefits provided by the Operator, including without limitation higher quotas, exclusive features, and priority support, as shown on the purchase page, the order, and the in-app description.
  4. +
  5. User means a natural person, or an entity duly authorized, who registers for and uses the App and purchases a membership service.
  6. +
+ +

2. Service

+

The specific content, term, and scope of the membership service are as displayed in the App, confirmed in the order, and described in the then-effective notices. The Operator may optimize, adjust, or discontinue certain benefits based on business plans, compliance requirements, or technical conditions, and will notify you by in-app notice, dialog, or another reasonable method. If a change materially and adversely affects benefits you have already paid for, the Operator will, to the extent permitted by law, provide a reasonable remedy or alternative.

+ +

3. Fees and payment

+
    +
  1. The membership service is a paid service. The price shown on the purchase page applies and may vary by promotion, region, or channel.
  2. +
  3. If you pay through a third-party channel (such as the App Store, WeChat, or Alipay), you must also follow that channel's rules.
  4. +
  5. If a membership includes automatic renewal, we will show the renewal amount and period before renewal, as required by the platform or by law. You may turn off automatic renewal in the relevant platform or by following the in-app instructions. Benefits for the current paid period remain available after you turn it off, and no further charge will be made after that period ends.
  6. +
+ +

4. Acceptable use

+

You must use the membership service lawfully and in good faith. You must not use it to break the law, infringe the rights of others, compromise system security, or interfere with other users. The Operator may restrict features, suspend the service, or end membership for a violation of this Agreement or the related rules.

+ +

5. Refunds and termination

+
    +
  1. Once a digital service is activated, it is generally non-refundable except as required by law, by an app store policy, or as otherwise promised by the Operator. If a charge results from a system fault, a duplicate charge, or another cause that is not your fault, contact support and we will verify it.
  2. +
  3. Fees already paid will not be refunded if your account or the service is restricted or terminated because you violated this Agreement or the related rules.
  4. +
  5. After the membership expires or ends, you will no longer receive member benefits. Retention of membership-related data follows the App's Privacy Policy.
  6. +
+ +

6. Disclaimer and limitation of liability

+

The membership service depends on networks, devices, and third-party services. The Operator will use reasonable efforts to keep the service available, but is not liable for interruption or data loss caused by force majeure, network failure, a third party, or any other cause outside the Operator's reasonable control. To the extent permitted by law, the Operator's liability for the membership service is limited to the amount you actually paid for the relevant order.

+ +

7. Changes

+

The Operator may revise this Agreement as laws change or as the service requires. Revisions will be notified by in-app notice or another appropriate method. Continued use of the membership service means you accept the revised Agreement. If you do not accept it, stop using the service and, where the rules allow, turn off automatic renewal.

+ +

8. Governing law and disputes

+

The formation, validity, interpretation, and dispute resolution of this Agreement are governed by the laws of the mainland of the People's Republic of China. Disputes arising out of this Agreement shall first be resolved through friendly consultation. If consultation fails, either party may bring a lawsuit before a people's court with jurisdiction at the defendant's domicile.

+ +

9. Contact us

+

If you have any question about this Agreement or the membership service, contact us at:
+ Company name: Shenzhen Yimai Intelligent Technology Co., Ltd.
+ App name: EAIMAR
+ Email: tech@eaimar.com
+ Telephone: +86 13265675086
+ Address: 4th Floor, Building A, Hongjin Industrial Park, No. 10 Huafang Road, Huarong Community, Dalang Street, Longhua District, Shenzhen, Guangdong Province, China

+

Shenzhen Yimai Intelligent Technology Co., Ltd.
October 24, 2026

+ + diff --git a/docs/eaimar/member_service_agreement_zh.html b/docs/eaimar/member_service_agreement_zh.html new file mode 100644 index 00000000..6ca39bb5 --- /dev/null +++ b/docs/eaimar/member_service_agreement_zh.html @@ -0,0 +1,69 @@ + + + + + + 会员服务协议 - EAIMAR + + + +

会员服务协议

+

EAIMAR 移动应用
更新日期:2026年10月24日
生效日期:2026年10月24日

+

欢迎您使用 EAIMAR(下称「本应用」)提供的会员服务。请您在购买、订阅或使用会员服务前,仔细阅读并充分理解本协议全部条款,特别是涉及费用、自动续费、退款、责任限制及争议解决的条款。您点击同意、完成支付或以其他方式接受会员服务的,即视为您已阅读并同意受本协议约束。

+

若您不同意本协议,请勿购买或使用会员服务。

+ +

一、定义

+
    +
  1. 运营方:指深圳翊迈智能科技有限公司,是本应用及会员服务的开发者和运营者。
  2. +
  3. 会员服务:指运营方向用户提供的付费会员权益,包括但不限于更高额度、专属功能、优先客服等,具体以购买页面、订单及本应用内说明为准。
  4. +
  5. 用户:指注册并使用本应用,且购买会员服务的自然人或经合法授权的主体。
  6. +
+ +

二、服务内容

+

会员服务的具体内容、有效期限、使用范围以本应用内展示、订单确认信息及届时有效的说明为准。运营方有权根据业务规划、合规要求、技术条件等对会员权益进行优化、调整或下线部分功能,并将通过应用内公告、弹窗或其他合理方式提示;若调整对您已付费权益产生重大不利影响的,运营方将在法律允许的范围内提供合理补偿或替代方案。

+ +

三、费用与支付

+
    +
  1. 会员服务为收费服务,价格以购买页面展示为准,可能因营销活动、地区或渠道不同而存在差异。
  2. +
  3. 您通过第三方支付渠道(如 App Store、微信、支付宝等)完成支付的,应同时遵守相应支付渠道的规则。
  4. +
  5. 若会员包含自动续费,我们将在续费前按平台规则或法律规定向您展示续费金额与周期;您可在相应平台或本应用内按指引关闭自动续费。关闭自动续费后,当前会员有效期内的权益不受影响,到期后将不再自动扣费。
  6. +
+ +

四、使用规范

+

您应合法、善意地使用会员服务,不得利用会员服务从事违法违规、侵犯他人权益、破坏系统安全或干扰其他用户正常使用的行为。运营方有权依据本协议及相关规则对违规行为采取限制功能、中止服务、终止会员资格等措施。

+ +

五、退款与终止

+
    +
  1. 数字服务一经开通,除法律法规规定、应用商店政策或运营方另行承诺的情形外,原则上不支持无理由退款。若因系统故障、重复扣费等非用户过错导致的问题,您可联系客服核实处理。
  2. +
  3. 因您违反本协议或相关规则导致账号或服务被限制、终止的,已支付费用不予退还。
  4. +
  5. 会员到期或终止后,您将无法继续享有会员权益,账号下与会员相关的数据保存期限以本应用《隐私政策》为准。
  6. +
+ +

六、免责声明与责任限制

+

会员服务依赖网络、设备、第三方服务等因素,运营方将尽力保障服务稳定,但不对因不可抗力、网络故障、第三方原因或非运营方可控原因导致的服务中断、数据丢失承担责任。在法律允许的范围内,运营方对会员服务所承担的赔偿责任以您就该笔订单实际支付的金额为上限。

+ +

七、协议变更

+

运营方可根据法律法规变化及业务需要修订本协议。修订后将通过应用内公告或其他适当方式通知;若您继续使用会员服务,即视为接受修订后的协议;若您不同意,应停止使用并可在规则允许范围内申请关闭自动续费。

+ +

八、适用法律与争议解决

+

本协议的订立、效力、解释及争议解决均适用中华人民共和国大陆地区法律。因本协议引起的争议,双方应友好协商;协商不成的,任何一方均可向被告住所地有管辖权的人民法院提起诉讼。

+ +

九、联系我们

+

如您对本协议或会员服务有任何疑问,可通过以下方式联系:
+ 公司名称:深圳翊迈智能科技有限公司
+ 应用名称:EAIMAR
+ 电子邮箱:tech@eaimar.com
+ 联系电话:13265675086
+ 通信地址:广东省深圳市龙华区大浪街道华荣社区华芳路 10 号鸿锦工业园 A 栋 4 层

+

深圳翊迈智能科技有限公司
2026年10月24日

+ +