|
@@ -16,12 +16,13 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
import java.util.Collections;
|
|
import java.util.Collections;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
-import java.util.Collections;
|
|
|
|
|
import java.util.Set;
|
|
import java.util.Set;
|
|
|
import java.util.UUID;
|
|
import java.util.UUID;
|
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
@@ -283,18 +284,17 @@ public class MembershipService implements MembershipServiceInterface {
|
|
|
|| "mascot_ai".equals(feature);
|
|
|| "mascot_ai".equals(feature);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // PROVIDER也有全部功能
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+ // 服务商(PROVIDER)为独立角色,不走会员权益体系;未知等级按无权限处理
|
|
|
|
|
+ return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 检查用户是否有指定功能的权限
|
|
* 检查用户是否有指定功能的权限
|
|
|
* <p>
|
|
* <p>
|
|
|
* 根据设计文档 Section 5.2:
|
|
* 根据设计文档 Section 5.2:
|
|
|
- * 1. 获取用户 memberLevel
|
|
|
|
|
|
|
+ * 1. 获取用户 memberLevel(getMemberLevel 已归一化:PROVIDER 服务商身份不构成会员等级,按 FREE 处理)
|
|
|
* 2. 查 membership_levels 表对应等级的 features JSON
|
|
* 2. 查 membership_levels 表对应等级的 features JSON
|
|
|
* 3. 检查 featureCode 是否在 features 数组中
|
|
* 3. 检查 featureCode 是否在 features 数组中
|
|
|
- * 4. 特殊:PROVIDER 类型还需校验 vendorStatus == 'approved'
|
|
|
|
|
*
|
|
*
|
|
|
* @param userId 用户ID
|
|
* @param userId 用户ID
|
|
|
* @param featureCode 功能代码(如 "free_activities", "discount_purchase")
|
|
* @param featureCode 功能代码(如 "free_activities", "discount_purchase")
|
|
@@ -312,10 +312,6 @@ public class MembershipService implements MembershipServiceInterface {
|
|
|
);
|
|
);
|
|
|
if (levelConfig == null || levelConfig.getFeatures() == null) return false;
|
|
if (levelConfig == null || levelConfig.getFeatures() == null) return false;
|
|
|
|
|
|
|
|
- if ("PROVIDER".equals(level) && !"approved".equals(user.getVendorStatus())) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
String featuresJson = levelConfig.getFeatures();
|
|
String featuresJson = levelConfig.getFeatures();
|
|
|
try {
|
|
try {
|
|
|
// features 格式: ["free_activities","free_courses",...] 或逗号分隔
|
|
// features 格式: ["free_activities","free_courses",...] 或逗号分隔
|
|
@@ -1261,6 +1257,10 @@ public class MembershipService implements MembershipServiceInterface {
|
|
|
|
|
|
|
|
// 个人会员身份(含有效期校验)
|
|
// 个人会员身份(含有效期校验)
|
|
|
String personal = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
|
|
String personal = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
|
|
|
|
|
+ // 服务商(PROVIDER)为独立角色,不构成会员等级身份,不参与会员权益判定
|
|
|
|
|
+ if ("PROVIDER".equals(personal)) {
|
|
|
|
|
+ personal = "FREE";
|
|
|
|
|
+ }
|
|
|
if ("FAMILY".equals(personal) && user.getMemberExpireTime() != null) {
|
|
if ("FAMILY".equals(personal) && user.getMemberExpireTime() != null) {
|
|
|
if (user.getMemberExpireTime().before(new Date())) {
|
|
if (user.getMemberExpireTime().before(new Date())) {
|
|
|
personal = "FREE";
|
|
personal = "FREE";
|
|
@@ -1275,7 +1275,7 @@ public class MembershipService implements MembershipServiceInterface {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 获取等级编码对应的级别数字(数字越小级别越低),未知等级回退 1(最低)
|
|
|
|
|
|
|
+ * 获取等级编码对应的级别数字(数字越大级别越高;PROVIDER=1 与 FREE 同级,视为非会员),未知等级回退 1(最低)
|
|
|
*/
|
|
*/
|
|
|
public int getLevelOrder(String levelCode) {
|
|
public int getLevelOrder(String levelCode) {
|
|
|
if (levelCode == null || levelCode.isEmpty()) {
|
|
if (levelCode == null || levelCode.isEmpty()) {
|
|
@@ -1681,4 +1681,163 @@ public class MembershipService implements MembershipServiceInterface {
|
|
|
);
|
|
);
|
|
|
return benefits;
|
|
return benefits;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 权益对比表(购买页/会员中心) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 生成会员权益对比表数据。
|
|
|
|
|
+ * 列:FREE / BASIC / FAMILY / PREMIUM(可购档 + 免费;LIFETIME 不可购、PROVIDER 为服务商角色均不展示)。
|
|
|
|
|
+ * 行全部来自事实源:
|
|
|
|
|
+ * - 商品折扣:sys_config member_discount_family/premium(bps)
|
|
|
|
|
+ * - 成员/任务上限:membership_levels.max_children / max_tasks_per_day
|
|
|
|
|
+ * - 月度次数:BasicQuotaService.QUOTA_LIMITS
|
|
|
|
|
+ * - 其余权益:membership_benefits 描述 + canUseFeature 功能开关
|
|
|
|
|
+ * 值均为展示文本,保证与实际权限一致。
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> getMembershipCompareData() {
|
|
|
|
|
+ List<String> columns = Arrays.asList("FREE", "BASIC", "FAMILY", "PREMIUM");
|
|
|
|
|
+
|
|
|
|
|
+ // 各等级配置(上限字段)
|
|
|
|
|
+ Map<String, MembershipLevel> levelByCode = new HashMap<>();
|
|
|
|
|
+ for (String code : columns) {
|
|
|
|
|
+ MembershipLevel lv = levelMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<MembershipLevel>().eq(MembershipLevel::getLevelCode, code).last("LIMIT 1"));
|
|
|
|
|
+ if (lv != null) {
|
|
|
|
|
+ levelByCode.put(code, lv);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 各等级权益描述(备用,后续可按 benefit_code 维度扩展对比行)
|
|
|
|
|
+ List<Map<String, Object>> rows = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 商品折扣:FREE=普通价格;BASIC=无;FAMILY=9折;PREMIUM=8折(bps 配置)
|
|
|
|
|
+ rows.add(compareRow("商品折扣", columns, code -> {
|
|
|
|
|
+ if ("FREE".equals(code)) return "普通价格";
|
|
|
|
|
+ if ("BASIC".equals(code)) return "—";
|
|
|
|
|
+ String cfgKey = "FAMILY".equals(code) ? "member_discount_family" : "member_discount_premium";
|
|
|
|
|
+ return formatDiscountBps(sysConfigService.getValue(cfgKey));
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 家庭成员上限
|
|
|
|
|
+ rows.add(compareRow("家庭成员上限", columns, code -> {
|
|
|
|
|
+ MembershipLevel lv = levelByCode.get(code);
|
|
|
|
|
+ return (lv != null && lv.getMaxChildren() != null && lv.getMaxChildren() > 0)
|
|
|
|
|
+ ? lv.getMaxChildren() + "人" : "—";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 每日任务上限
|
|
|
|
|
+ rows.add(compareRow("每日任务上限", columns, code -> {
|
|
|
|
|
+ MembershipLevel lv = levelByCode.get(code);
|
|
|
|
|
+ return (lv != null && lv.getMaxTasksPerDay() != null && lv.getMaxTasksPerDay() > 0)
|
|
|
|
|
+ ? lv.getMaxTasksPerDay() + "个/日" : "—";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 优惠券:FREE=—;BASIC=平台券;FAMILY/PREMIUM=平台+会员券
|
|
|
|
|
+ rows.add(compareRow("优惠券权益", columns, code -> {
|
|
|
|
|
+ if ("FREE".equals(code)) return "—";
|
|
|
|
|
+ if ("BASIC".equals(code)) return "平台优惠券";
|
|
|
|
|
+ return "平台券+会员专享券";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 5. AI健康顾问:BASIC 限次,FAMILY+ 不限
|
|
|
|
|
+ rows.add(compareRow("AI健康顾问", columns, code -> {
|
|
|
|
|
+ if ("FREE".equals(code)) return "—";
|
|
|
|
|
+ if ("BASIC".equals(code)) return "每月" + BasicQuotaService.getQuotaLimit("AI_CHAT") + "次";
|
|
|
|
|
+ return "不限次数";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 报告解读:BASIC 限次,FAMILY 不限,PREMIUM 深度
|
|
|
|
|
+ rows.add(compareRow("报告解读", columns, code -> {
|
|
|
|
|
+ if ("FREE".equals(code)) return "—";
|
|
|
|
|
+ if ("BASIC".equals(code)) return "每月" + BasicQuotaService.getQuotaLimit("REPORT_READING") + "次";
|
|
|
|
|
+ if ("PREMIUM".equals(code)) return "不限·深度解读";
|
|
|
|
|
+ return "不限次数";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 测评预约:FREE 仅查看;BASIC 限次;FAMILY 不限;PREMIUM 优先
|
|
|
|
|
+ rows.add(compareRow("测评预约", columns, code -> {
|
|
|
|
|
+ if ("FREE".equals(code)) return "仅查看";
|
|
|
|
|
+ if ("BASIC".equals(code)) return "每月" + BasicQuotaService.getQuotaLimit("ASSESSMENT_QUOTA") + "次";
|
|
|
|
|
+ if ("PREMIUM".equals(code)) return "不限·优先";
|
|
|
|
|
+ return "不限次数";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 8. 食材图片解析:BASIC 限次,FAMILY+ 不限
|
|
|
|
|
+ rows.add(compareRow("食材图片解析", columns, code -> {
|
|
|
|
|
+ if ("FREE".equals(code)) return "—";
|
|
|
|
|
+ if ("BASIC".equals(code)) return "每月" + BasicQuotaService.getQuotaLimit("FOOD_RECOGNIZE") + "次";
|
|
|
|
|
+ return "不限次数";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 9. 专属活动:FREE/BASIC 部分;FAMILY 全部;PREMIUM 优先
|
|
|
|
|
+ rows.add(compareRow("会员专属活动", columns, code -> {
|
|
|
|
|
+ if ("FREE".equals(code) || "BASIC".equals(code)) return "部分活动";
|
|
|
|
|
+ if ("PREMIUM".equals(code)) return "优先参与";
|
|
|
|
|
+ return "全部活动";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 10. 邀请家人佣金:BASIC+ 可参与
|
|
|
|
|
+ rows.add(compareRow("邀请佣金", columns, code -> "FREE".equals(code) ? "—" : "可参与"));
|
|
|
|
|
+
|
|
|
|
|
+ // 11. 消费返佣:FAMILY+ 可参与
|
|
|
|
|
+ rows.add(compareRow("消费返佣", columns, code -> {
|
|
|
|
|
+ if ("FREE".equals(code) || "BASIC".equals(code)) return "—";
|
|
|
|
|
+ return "可参与";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 12. 管家服务:FAMILY 月1次;PREMIUM 周1次
|
|
|
|
|
+ rows.add(compareRow("专属管家", columns, code -> {
|
|
|
|
|
+ if ("FAMILY".equals(code)) return "每月1次";
|
|
|
|
|
+ if ("PREMIUM".equals(code)) return "每周1次";
|
|
|
|
|
+ return "—";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 13. 成长规划师咨询:PREMIUM 专属
|
|
|
|
|
+ rows.add(compareRow("规划师咨询", columns, code -> {
|
|
|
|
|
+ if ("PREMIUM".equals(code)) return "每月1次";
|
|
|
|
|
+ return "—";
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 14. 免费课程/活动(全员可用基准项)
|
|
|
|
|
+ rows.add(compareRow("免费课程与活动", columns, code -> "可参与"));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("columns", columns);
|
|
|
|
|
+ result.put("rows", rows);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构造一行对比数据:row = {name, <levelCode小写>: 值}。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, Object> compareRow(String name, List<String> columns,
|
|
|
|
|
+ java.util.function.Function<String, String> valueFn) {
|
|
|
|
|
+ Map<String, Object> row = new LinkedHashMap<>();
|
|
|
|
|
+ row.put("name", name);
|
|
|
|
|
+ for (String code : columns) {
|
|
|
|
|
+ String v = valueFn.apply(code);
|
|
|
|
|
+ row.put(code.toLowerCase(), v == null ? "—" : v);
|
|
|
|
|
+ }
|
|
|
|
|
+ return row;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 折扣 bps 转展示文本:9000 → "9折",8500 → "8.5折";非法值返回 "—"。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String formatDiscountBps(String cfgValue) {
|
|
|
|
|
+ if (cfgValue == null || cfgValue.isEmpty()) {
|
|
|
|
|
+ return "—";
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ int bps = Integer.parseInt(cfgValue);
|
|
|
|
|
+ if (bps <= 0 || bps >= 10000) {
|
|
|
|
|
+ return "—";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (bps % 1000 == 0) {
|
|
|
|
|
+ return (bps / 1000) + "折";
|
|
|
|
|
+ }
|
|
|
|
|
+ return String.format(java.util.Locale.ROOT, "%.1f折", bps / 1000.0);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return "—";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|