|
|
@@ -0,0 +1,414 @@
|
|
|
+package com.fs.his.service;
|
|
|
+
|
|
|
+import cn.hutool.json.JSONArray;
|
|
|
+import cn.hutool.json.JSONObject;
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
+import com.fs.common.exception.CustomException;
|
|
|
+import com.fs.common.utils.StringUtils;
|
|
|
+import com.fs.his.config.CouponConfig;
|
|
|
+import com.fs.his.domain.FsNewcomerQuestionnaire;
|
|
|
+import com.fs.his.domain.FsUser;
|
|
|
+import com.fs.his.param.FsUserCouponSendParam;
|
|
|
+import com.fs.his.utils.PhoneUtil;
|
|
|
+import com.fs.his.vo.NewcomerWelfareStateVO;
|
|
|
+import com.fs.system.service.ISysConfigService;
|
|
|
+import lombok.Data;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import javax.validation.constraints.NotNull;
|
|
|
+import java.io.Serializable;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 新手福利:手机号奇偶分流、动态问卷、发券
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class NewcomerWelfareService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IFsUserService fsUserService;
|
|
|
+ @Autowired
|
|
|
+ private IFsUserCouponService fsUserCouponService;
|
|
|
+ @Autowired
|
|
|
+ private NewcomerQuestionnaireService questionnaireService;
|
|
|
+ @Autowired
|
|
|
+ private ISysConfigService configService;
|
|
|
+
|
|
|
+ @Data
|
|
|
+ public static class SubmitBody implements Serializable {
|
|
|
+ @NotNull
|
|
|
+ private Long questionnaireId;
|
|
|
+ @NotNull
|
|
|
+ private Map<String, Object> answers;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public NewcomerWelfareStateVO getState(Long userId) {
|
|
|
+ FsUser user = fsUserService.selectFsUserByUserId(userId);
|
|
|
+ if (user == null) {
|
|
|
+ throw new CustomException("用户不存在");
|
|
|
+ }
|
|
|
+ assignCouponTypeIfNeeded(user);
|
|
|
+
|
|
|
+ NewcomerWelfareStateVO vo = new NewcomerWelfareStateVO();
|
|
|
+ String plain = resolvePlainPhone(user);
|
|
|
+ vo.setHasPhone(StringUtils.isNotBlank(plain));
|
|
|
+ vo.setNewcomerCouponType(user.getNewcomerCouponType());
|
|
|
+
|
|
|
+ boolean eligible = isEligibleByAccountAge(user);
|
|
|
+ boolean granted = isGranted(user);
|
|
|
+ vo.setWelfareGranted(granted);
|
|
|
+
|
|
|
+ FsNewcomerQuestionnaire q = questionnaireService.resolveForApp();
|
|
|
+ fillQuestionnaire(vo, q);
|
|
|
+
|
|
|
+ boolean pathA = "A".equals(user.getNewcomerCouponType());
|
|
|
+ boolean hasForm = q != null && StringUtils.isNotBlank(q.getFormSchema());
|
|
|
+ vo.setShowQuestionnaire(vo.isHasPhone() && eligible && pathA && !granted && !isProfileDone(user) && hasForm);
|
|
|
+
|
|
|
+ vo.setJustGranted(false);
|
|
|
+ if (vo.isHasPhone() && eligible && "B".equals(user.getNewcomerCouponType()) && !granted) {
|
|
|
+ tryGrantAndMark(user, "B");
|
|
|
+ vo.setJustGranted(true);
|
|
|
+ user = fsUserService.selectFsUserByUserId(userId);
|
|
|
+ vo.setWelfareGranted(isGranted(user));
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void submitQuestionnaire(Long userId, SubmitBody body) {
|
|
|
+ FsUser user = fsUserService.selectFsUserByUserId(userId);
|
|
|
+ if (user == null) {
|
|
|
+ throw new CustomException("用户不存在");
|
|
|
+ }
|
|
|
+ assignCouponTypeIfNeeded(user);
|
|
|
+ if (!"A".equals(user.getNewcomerCouponType())) {
|
|
|
+ throw new CustomException("当前账号无需提交问卷");
|
|
|
+ }
|
|
|
+ if (isGranted(user)) {
|
|
|
+ throw new CustomException("新手福利已领取");
|
|
|
+ }
|
|
|
+ if (isProfileDone(user)) {
|
|
|
+ throw new CustomException("问卷已完成");
|
|
|
+ }
|
|
|
+ if (!isEligibleByAccountAge(user)) {
|
|
|
+ throw new CustomException("当前账号不参与新手福利活动");
|
|
|
+ }
|
|
|
+
|
|
|
+ FsNewcomerQuestionnaire current = questionnaireService.resolveForApp();
|
|
|
+ if (current == null || current.getId() == null) {
|
|
|
+ throw new CustomException("问卷未配置");
|
|
|
+ }
|
|
|
+ if (!current.getId().equals(body.getQuestionnaireId())) {
|
|
|
+ throw new CustomException("问卷已更新,请返回刷新后重新填写");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(current.getFormSchema())) {
|
|
|
+ throw new CustomException("问卷表单为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ validateAnswers(current.getFormSchema(), body.getAnswers());
|
|
|
+
|
|
|
+ FsUser patch = new FsUser();
|
|
|
+ patch.setUserId(userId);
|
|
|
+ patch.setNewcomerQuestionnaireId(current.getId());
|
|
|
+ patch.setNewcomerAnswersJson(JSONUtil.toJsonStr(body.getAnswers()));
|
|
|
+ patch.setNewcomerProfileDone(1);
|
|
|
+ syncSexFromAnswers(patch, body.getAnswers());
|
|
|
+ fsUserService.updateFsUser(patch);
|
|
|
+
|
|
|
+ user.setNewcomerProfileDone(1);
|
|
|
+ tryGrantAndMark(user, "A");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillQuestionnaire(NewcomerWelfareStateVO vo, FsNewcomerQuestionnaire q) {
|
|
|
+ if (q == null) {
|
|
|
+ vo.setQuestionnaireId(null);
|
|
|
+ vo.setFormSchema(null);
|
|
|
+ vo.setSchemaVersion(1);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ vo.setQuestionnaireId(q.getId());
|
|
|
+ vo.setFormSchema(q.getFormSchema());
|
|
|
+ vo.setSchemaVersion(parseSchemaVersion(q.getFormSchema()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int parseSchemaVersion(String formSchema) {
|
|
|
+ if (StringUtils.isBlank(formSchema)) {
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Integer v = JSONUtil.parseObj(formSchema).getInt("version");
|
|
|
+ return v != null && v > 0 ? v : 1;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assignCouponTypeIfNeeded(FsUser user) {
|
|
|
+ if (StringUtils.isNotBlank(user.getNewcomerCouponType())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String plain = resolvePlainPhone(user);
|
|
|
+ if (StringUtils.isBlank(plain)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ char last = plain.charAt(plain.length() - 1);
|
|
|
+ int d = (last >= '0' && last <= '9') ? (last - '0') : 0;
|
|
|
+ String type = (last >= '0' && last <= '9' && d % 2 == 1) ? "A" : "B";
|
|
|
+
|
|
|
+ FsUser patch = new FsUser();
|
|
|
+ patch.setUserId(user.getUserId());
|
|
|
+ patch.setNewcomerCouponType(type);
|
|
|
+ fsUserService.updateFsUser(patch);
|
|
|
+ user.setNewcomerCouponType(type);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolvePlainPhone(FsUser user) {
|
|
|
+ if (user == null || StringUtils.isBlank(user.getPhone())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String p = user.getPhone().trim();
|
|
|
+ if (p.matches("^1\\d{10}$")) {
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+ String d = PhoneUtil.decryptPhone(p);
|
|
|
+ if (StringUtils.isNotBlank(d) && d.matches("^1\\d{10}$")) {
|
|
|
+ return d;
|
|
|
+ }
|
|
|
+ d = PhoneUtil.decryptPhoneOldKey(p);
|
|
|
+ if (StringUtils.isNotBlank(d) && d.matches("^1\\d{10}$")) {
|
|
|
+ return d;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isEligibleByAccountAge(FsUser user) {
|
|
|
+ String daysStr = configService.selectConfigByKey("newcomer.welfare.max_account_age_days");
|
|
|
+ if (StringUtils.isBlank(daysStr)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ int days;
|
|
|
+ try {
|
|
|
+ days = Integer.parseInt(daysStr.trim());
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (days <= 0 || user.getCreateTime() == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ long cutoff = System.currentTimeMillis() - (long) days * 86_400_000L;
|
|
|
+ return user.getCreateTime().getTime() >= cutoff;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isGranted(FsUser user) {
|
|
|
+ return user.getNewcomerWelfareGranted() != null && user.getNewcomerWelfareGranted() == 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isProfileDone(FsUser user) {
|
|
|
+ return user.getNewcomerProfileDone() != null && user.getNewcomerProfileDone() == 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void tryGrantAndMark(FsUser user, String path) {
|
|
|
+ Long couponId = resolveCouponId(path);
|
|
|
+ if (couponId == null) {
|
|
|
+ throw new CustomException("未配置新手福利券,请在参数 his.coupon 中配置 newcomerCoupon" + path);
|
|
|
+ }
|
|
|
+ FsUserCouponSendParam sendParam = new FsUserCouponSendParam();
|
|
|
+ sendParam.setUserId(user.getUserId());
|
|
|
+ sendParam.setCouponId(couponId);
|
|
|
+ sendParam.setSystemSend(true);
|
|
|
+ fsUserCouponService.sendFsUserCoupon(sendParam);
|
|
|
+
|
|
|
+ FsUser patch = new FsUser();
|
|
|
+ patch.setUserId(user.getUserId());
|
|
|
+ patch.setNewcomerWelfareGranted(1);
|
|
|
+ fsUserService.updateFsUser(patch);
|
|
|
+ user.setNewcomerWelfareGranted(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long resolveCouponId(String path) {
|
|
|
+ String json = configService.selectConfigByKey("his.coupon");
|
|
|
+ if (StringUtils.isBlank(json)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ CouponConfig cfg = JSONUtil.toBean(json, CouponConfig.class);
|
|
|
+ if (cfg == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return "A".equals(path) ? cfg.getNewcomerCouponA() : cfg.getNewcomerCouponB();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void syncSexFromAnswers(FsUser patch, Map<String, Object> answers) {
|
|
|
+ if (answers == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Object s = answers.get("sex");
|
|
|
+ if (s == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int v = Integer.parseInt(String.valueOf(s).trim());
|
|
|
+ if (v == 1 || v == 2) {
|
|
|
+ patch.setSex(v);
|
|
|
+ }
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 答案与 form_schema 校验(原独立类内聚至此) ----------
|
|
|
+ /** 展示名:优先 label,兼容仅设计器使用的 title */
|
|
|
+ private static String fieldDisplay(JSONObject f, String keyFallback) {
|
|
|
+ if (f == null) {
|
|
|
+ return keyFallback;
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(f.getStr("label"))) {
|
|
|
+ return f.getStr("label");
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(f.getStr("title"))) {
|
|
|
+ return f.getStr("title");
|
|
|
+ }
|
|
|
+ return keyFallback;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateAnswers(String formSchemaJson, Map<String, Object> answers) {
|
|
|
+ if (StringUtils.isBlank(formSchemaJson)) {
|
|
|
+ throw new CustomException("问卷未配置表单");
|
|
|
+ }
|
|
|
+ JSONArray fields = JSONUtil.parseObj(formSchemaJson).getJSONArray("fields");
|
|
|
+ if (fields == null || fields.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (int i = 0; i < fields.size(); i++) {
|
|
|
+ JSONObject f = fields.getJSONObject(i);
|
|
|
+ if (f == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String key = f.getStr("key");
|
|
|
+ if (StringUtils.isBlank(key)) {
|
|
|
+ throw new CustomException("问卷配置异常:题目缺少 key");
|
|
|
+ }
|
|
|
+ String type = StringUtils.defaultString(f.getStr("type"), "text").toLowerCase();
|
|
|
+ boolean required = f.getBool("required", false);
|
|
|
+ Object raw = answers == null ? null : answers.get(key);
|
|
|
+
|
|
|
+ if (required && isAnswerEmpty(raw, type)) {
|
|
|
+ throw new CustomException("请填写:" + fieldDisplay(f, key));
|
|
|
+ }
|
|
|
+ if (isAnswerEmpty(raw, type)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ switch (type) {
|
|
|
+ case "radio":
|
|
|
+ assertRadio(f, raw);
|
|
|
+ break;
|
|
|
+ case "checkbox":
|
|
|
+ assertCheckbox(f, raw);
|
|
|
+ break;
|
|
|
+ case "number":
|
|
|
+ assertNumber(raw, fieldDisplay(f, key));
|
|
|
+ break;
|
|
|
+ case "text":
|
|
|
+ default:
|
|
|
+ if (!(raw instanceof String) && raw != null) {
|
|
|
+ throw new CustomException("题目格式不正确:" + fieldDisplay(f, key));
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isAnswerEmpty(Object raw, String type) {
|
|
|
+ if (raw == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if ("checkbox".equals(type)) {
|
|
|
+ List<?> list = normalizeList(raw);
|
|
|
+ return list == null || list.isEmpty();
|
|
|
+ }
|
|
|
+ if (raw instanceof String) {
|
|
|
+ return StringUtils.isBlank((String) raw);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void assertRadio(JSONObject field, Object raw) {
|
|
|
+ String v = raw == null ? null : String.valueOf(raw).trim();
|
|
|
+ if (!optionValues(field).contains(v)) {
|
|
|
+ throw new CustomException("选项不合法:" + fieldDisplay(field, field.getStr("key")));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void assertCheckbox(JSONObject field, Object raw) {
|
|
|
+ List<?> list = normalizeList(raw);
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Set<String> allowed = optionValues(field);
|
|
|
+ for (Object o : list) {
|
|
|
+ String v = o == null ? "" : String.valueOf(o).trim();
|
|
|
+ if (!allowed.contains(v)) {
|
|
|
+ throw new CustomException("多选项不合法:" + fieldDisplay(field, field.getStr("key")));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void assertNumber(Object raw, String label) {
|
|
|
+ if (raw instanceof Number) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (raw instanceof String) {
|
|
|
+ try {
|
|
|
+ Double.parseDouble(((String) raw).trim());
|
|
|
+ return;
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ // fall through
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new CustomException("请输入有效数字:" + label);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Set<String> optionValues(JSONObject field) {
|
|
|
+ Set<String> set = new HashSet<>();
|
|
|
+ JSONArray opts = field.getJSONArray("options");
|
|
|
+ if (opts == null) {
|
|
|
+ return set;
|
|
|
+ }
|
|
|
+ for (int j = 0; j < opts.size(); j++) {
|
|
|
+ JSONObject o = opts.getJSONObject(j);
|
|
|
+ if (o != null && o.get("value") != null) {
|
|
|
+ set.add(String.valueOf(o.get("value")).trim());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return set;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<?> normalizeList(Object raw) {
|
|
|
+ if (raw == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (raw instanceof List) {
|
|
|
+ return (List<?>) raw;
|
|
|
+ }
|
|
|
+ if (raw instanceof Collection) {
|
|
|
+ return new ArrayList<>((Collection<?>) raw);
|
|
|
+ }
|
|
|
+ if (raw instanceof JSONArray) {
|
|
|
+ JSONArray ja = (JSONArray) raw;
|
|
|
+ List<Object> list = new ArrayList<>(ja.size());
|
|
|
+ for (int i = 0; i < ja.size(); i++) {
|
|
|
+ list.add(ja.get(i));
|
|
|
+ }
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|