UserService.java 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665
  1. package com.etotem.cfc.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.etotem.cfc.config.JwtConfig;
  4. import com.etotem.cfc.dto.*;
  5. import com.etotem.cfc.dto.RegisterWithIdCardDTO;
  6. import com.etotem.cfc.dto.RegisterWithInviteCodeDTO;
  7. import com.etotem.cfc.entity.FamilyMember;
  8. import com.etotem.cfc.entity.Family;
  9. import com.etotem.cfc.entity.User;
  10. import com.etotem.cfc.mapper.FamilyMemberMapper;
  11. import com.etotem.cfc.mapper.FamilyMapper;
  12. import com.etotem.cfc.mapper.UserMapper;
  13. import com.etotem.cfc.service.VerificationCodeService;
  14. import com.etotem.cfc.entity.DistributionRelation;
  15. import com.etotem.cfc.service.DistributionService;
  16. import com.etotem.cfc.common.Result;
  17. import com.etotem.cfc.service.api.UserServiceInterface;
  18. import lombok.extern.slf4j.Slf4j;
  19. import javax.annotation.Resource;
  20. import org.springframework.stereotype.Service;
  21. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  22. import org.springframework.util.DigestUtils;
  23. import java.nio.charset.StandardCharsets;
  24. import java.util.*;
  25. import com.etotem.cfc.entity.UserProfileHistory;
  26. import com.etotem.cfc.mapper.UserProfileHistoryMapper;
  27. import com.etotem.cfc.service.FamilyJoinRequestService;
  28. @Slf4j
  29. @Service
  30. public class UserService implements UserServiceInterface {
  31. @Resource
  32. private UserMapper userMapper;
  33. @Resource
  34. private FamilyMapper familyMapper;
  35. @Resource
  36. private FamilyMemberMapper familyMemberMapper;
  37. @Resource
  38. private JwtConfig jwtConfig;
  39. @Resource
  40. private VerificationCodeService verificationCodeService;
  41. @Resource
  42. private WechatService wechatService;
  43. @Resource
  44. private DanshopSyncService danshopSyncService;
  45. @Resource
  46. private DistributionService distributionService;
  47. @Resource
  48. private OnboardingService onboardingService;
  49. @Resource
  50. private FamilyInvitationService familyInvitationService;
  51. @Resource
  52. private UserProfileHistoryMapper userProfileHistoryMapper;
  53. @Resource
  54. private FamilyMemberService familyMemberService;
  55. @Resource
  56. private FamilyJoinRequestService familyJoinRequestService;
  57. public LoginResultDTO wechatLogin(WechatLoginDTO dto) {
  58. // 1. 通过code获取openid
  59. Map<String, String> sessionData = wechatService.code2Session(dto.getCode());
  60. String openid = sessionData.get("openid");
  61. String unionid = sessionData.get("unionid");
  62. if (openid == null || openid.isEmpty()) {
  63. throw new RuntimeException("获取openid失败,请重试");
  64. }
  65. // 2. 如果提供了phoneCode,获取真实手机号
  66. String phone = null;
  67. if (dto.getPhoneCode() != null && !dto.getPhoneCode().isEmpty()) {
  68. phone = wechatService.getPhoneNumber(dto.getPhoneCode());
  69. }
  70. // 3. 先通过openid查找用户
  71. User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
  72. .eq(User::getOpenid, openid));
  73. boolean isNewUser = false;
  74. if (user == null) {
  75. // 4. openid不存在,如果有手机号则通过手机号查找
  76. if (phone != null) {
  77. user = userMapper.selectOne(new LambdaQueryWrapper<User>()
  78. .eq(User::getPhone, phone));
  79. }
  80. if (user == null) {
  81. // 5. 完全查不到 → 全新用户,自动创建
  82. isNewUser = true;
  83. user = new User();
  84. user.setOpenid(openid);
  85. user.setUnionid(unionid != null ? unionid : "");
  86. user.setRole("parent");
  87. user.setNickname(dto.getNickname() != null ? dto.getNickname() : (phone != null ? "用户" + phone.substring(7) : "微信用户"));
  88. user.setAvatar(dto.getAvatar());
  89. user.setPhone(phone);
  90. user.setPassword("");
  91. user.setSessionKey(sessionData.get("session_key"));
  92. user.setCreatedAt(new Date());
  93. user.setUpdatedAt(new Date());
  94. createUserAndFamily(user);
  95. try { onboardingService.initForUser(user.getId()); onboardingService.completeTask(user.getId(), "REGISTER"); } catch (Exception e) { /* onboarding init failure should not block login */ }
  96. } else {
  97. // 6. 手机号已存在但openid不同,绑定openid
  98. user.setOpenid(openid);
  99. user.setUnionid(unionid != null ? unionid : "");
  100. user.setSessionKey(sessionData.get("session_key"));
  101. user.setUpdatedAt(new Date());
  102. userMapper.updateById(user);
  103. }
  104. } else {
  105. // 7. openid已存在,刷新sessionKey(虚拟支付signature签名必需,每次登录更新)+ 更新手机号(如果手机号变了)
  106. user.setSessionKey(sessionData.get("session_key"));
  107. if (phone != null && !phone.equals(user.getPhone())) {
  108. user.setPhone(phone);
  109. }
  110. user.setUpdatedAt(new Date());
  111. userMapper.updateById(user);
  112. }
  113. ensureUserHasFamily(user);
  114. // 管理员不能登录小程序
  115. if ("admin".equals(user.getRole())) {
  116. throw new RuntimeException("该账号不允许登录小程序");
  117. }
  118. // 生成token
  119. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  120. LoginResultDTO result = new LoginResultDTO();
  121. result.setToken(token);
  122. result.setUserId(user.getId());
  123. result.setRole(user.getRole());
  124. result.setRoles(user.getRoles());
  125. result.setFamilyId(user.getFamilyId());
  126. result.setNickname(user.getNickname());
  127. result.setIsNewUser(isNewUser);
  128. result.setOpenid(openid);
  129. // 新用户需要完善信息(仅当需要选择角色时)
  130. result.setNeedsRoleSelect(isNewUser);
  131. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  132. populateTeacherFields(result, user);
  133. return result;
  134. }
  135. /**
  136. * 手机号验证码登录
  137. */
  138. public LoginResultDTO phoneLogin(PhoneLoginDTO dto) {
  139. // 使用手机号作为唯一标识
  140. String phone = dto.getPhone();
  141. // 先通过手机号查找用户(更准确的判断)
  142. User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
  143. .eq(User::getPhone, phone));
  144. // 管理员不能登录小程序
  145. if (user != null && "admin".equals(user.getRole())) {
  146. throw new RuntimeException("该账号不允许登录小程序");
  147. }
  148. boolean isNewUser = false;
  149. if (user == null) {
  150. // 新用户,仅创建用户(家庭待首次使用家庭功能时创建)
  151. isNewUser = true;
  152. user = new User();
  153. user.setOpenid("phone:" + phone);
  154. user.setUnionid("");
  155. // 默认角色为家长,可以被覆盖
  156. user.setRole(dto.getRole() != null ? dto.getRole() : "parent");
  157. user.setNickname(dto.getNickname() != null ? dto.getNickname() : "用户" + phone.substring(7));
  158. user.setAvatar(dto.getAvatar());
  159. user.setPhone(phone);
  160. user.setBirthday(dto.getBirthday());
  161. user.setGender(dto.getGender());
  162. user.setPassword("");
  163. user.setCreatedAt(new Date());
  164. user.setUpdatedAt(new Date());
  165. createUserAndFamily(user);
  166. try { onboardingService.initForUser(user.getId()); onboardingService.completeTask(user.getId(), "REGISTER"); } catch (Exception e) { /* onboarding init failure should not block login */ }
  167. } else {
  168. // 老用户,更新信息(如果提供了)
  169. if (dto.getNickname() != null && !dto.getNickname().isEmpty()) {
  170. user.setNickname(dto.getNickname());
  171. }
  172. if (dto.getAvatar() != null && !dto.getAvatar().isEmpty()) {
  173. user.setAvatar(dto.getAvatar());
  174. }
  175. user.setUpdatedAt(new Date());
  176. userMapper.updateById(user);
  177. }
  178. ensureUserHasFamily(user);
  179. // 生成token
  180. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  181. LoginResultDTO result = new LoginResultDTO();
  182. result.setToken(token);
  183. result.setUserId(user.getId());
  184. result.setRole(user.getRole());
  185. result.setRoles(user.getRoles());
  186. result.setFamilyId(user.getFamilyId());
  187. result.setNickname(user.getNickname());
  188. result.setIsNewUser(isNewUser);
  189. result.setNeedsRoleSelect(isNewUser);
  190. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  191. populateTeacherFields(result, user);
  192. return result;
  193. }
  194. /**
  195. * 微信手机号登录(委托给wechatLogin处理)
  196. */
  197. public LoginResultDTO wechatPhoneLogin(WechatPhoneLoginDTO dto) {
  198. WechatLoginDTO mergedDto = new WechatLoginDTO();
  199. mergedDto.setCode(dto.getCode());
  200. mergedDto.setPhoneCode(dto.getPhoneCode());
  201. mergedDto.setNickname(dto.getNickname());
  202. mergedDto.setAvatar(dto.getAvatar());
  203. return wechatLogin(mergedDto);
  204. }
  205. /**
  206. * 微信openid静默登录(用户再次打开小程序时自动登录)
  207. */
  208. public LoginResultDTO silentLogin(String code) {
  209. // 1. 通过code获取openid
  210. Map<String, String> sessionData = wechatService.code2Session(code);
  211. String openid = sessionData.get("openid");
  212. String unionid = sessionData.get("unionid");
  213. // 2. 查找用户
  214. User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
  215. .eq(User::getOpenid, openid));
  216. if (user == null) {
  217. throw new RuntimeException("用户不存在,请先登录");
  218. }
  219. // 管理员不能登录小程序
  220. if ("admin".equals(user.getRole())) {
  221. throw new RuntimeException("该账号不允许登录小程序");
  222. }
  223. // 持久化最新 sessionKey(虚拟支付 signature 签名用)
  224. user.setSessionKey(sessionData.get("session_key"));
  225. user.setUpdatedAt(new Date());
  226. userMapper.updateById(user);
  227. ensureUserHasFamily(user);
  228. // 4. 生成token
  229. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  230. LoginResultDTO result = new LoginResultDTO();
  231. result.setToken(token);
  232. result.setUserId(user.getId());
  233. result.setRole(user.getRole());
  234. result.setRoles(user.getRoles());
  235. result.setFamilyId(user.getFamilyId());
  236. result.setNickname(user.getNickname());
  237. result.setIsNewUser(false);
  238. result.setOpenid(openid);
  239. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  240. populateTeacherFields(result, user);
  241. return result;
  242. }
  243. /**
  244. * 通过openid自动登录(前端已缓存openid时调用)
  245. */
  246. public LoginResultDTO autoLoginByOpenid(String openid) {
  247. User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
  248. .eq(User::getOpenid, openid));
  249. if (user == null) {
  250. return null;
  251. }
  252. // 管理员不能登录小程序
  253. if ("admin".equals(user.getRole())) {
  254. return null;
  255. }
  256. ensureUserHasFamily(user);
  257. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  258. LoginResultDTO result = new LoginResultDTO();
  259. result.setToken(token);
  260. result.setUserId(user.getId());
  261. result.setRole(user.getRole());
  262. result.setFamilyId(user.getFamilyId());
  263. result.setNickname(user.getNickname());
  264. result.setIsNewUser(false);
  265. result.setOpenid(openid);
  266. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  267. populateTeacherFields(result, user);
  268. return result;
  269. }
  270. public boolean setPassword(Long userId, String password) {
  271. User user = userMapper.selectById(userId);
  272. if (user == null) {
  273. return false;
  274. }
  275. user.setPassword(encryptPassword(password));
  276. user.setUpdatedAt(new Date());
  277. userMapper.updateById(user);
  278. return true;
  279. }
  280. public boolean verifyPassword(Long userId, String password) {
  281. User user = userMapper.selectById(userId);
  282. if (user == null || user.getPassword() == null || user.getPassword().isEmpty()) {
  283. return false;
  284. }
  285. if (!verifyPasswordHash(password, user.getPassword())) {
  286. return false;
  287. }
  288. // 旧 MD5 哈希自动升级为 BCrypt
  289. if (!user.getPassword().startsWith("$2")) {
  290. user.setPassword(BC_PASSWORD_ENCODER.encode(password));
  291. user.setUpdatedAt(new Date());
  292. userMapper.updateById(user);
  293. log.info("User {} 密码已从 MD5 升级为 BCrypt", userId);
  294. }
  295. return true;
  296. }
  297. public User getUserInfo(Long userId) {
  298. return userMapper.selectById(userId);
  299. }
  300. /** 回写字段白名单(user 目标) */
  301. private static final String[] USER_BACKFILL_WHITELIST = {"phone", "gender", "birthday", "birthHour", "dietPreferences", "address"};
  302. /** 回写字段白名单(member 目标,family_members 表无 dietPreferences/address 列) */
  303. private static final String[] MEMBER_BACKFILL_WHITELIST = {"phone", "gender", "birthday", "birthHour"};
  304. /**
  305. * 就地补充信息回写(字段白名单校验 + 目标归属校验)
  306. * @return Map: { updatedFields: List<String>, profile: Object(最新完整信息) }
  307. */
  308. public Map<String, Object> backfillProfile(Long userId, ProfileBackfillDTO dto) {
  309. if (dto == null || dto.getFields() == null || dto.getFields().isEmpty()) {
  310. throw new RuntimeException("回写字段不能为空");
  311. }
  312. User me = userMapper.selectById(userId);
  313. if (me == null) {
  314. throw new RuntimeException("用户不存在");
  315. }
  316. List<String> updatedFields = new ArrayList<>();
  317. Object profile;
  318. if ("user".equals(dto.getTargetType())) {
  319. // 仅允许回写自己
  320. if (dto.getTargetId() != null && !dto.getTargetId().equals(userId)) {
  321. throw new RuntimeException("仅能回写本人的个人信息");
  322. }
  323. for (String field : USER_BACKFILL_WHITELIST) {
  324. String value = dto.getFields().get(field);
  325. if (value == null || value.isEmpty()) continue;
  326. applyUserBackfillField(me, field, value.trim());
  327. updatedFields.add(field);
  328. }
  329. me.setUpdatedAt(new Date());
  330. userMapper.updateById(me);
  331. profile = me;
  332. } else if ("member".equals(dto.getTargetType())) {
  333. Long memberId = dto.getTargetId();
  334. if (memberId == null) {
  335. throw new RuntimeException("回写家庭成员时 targetId 不能为空");
  336. }
  337. FamilyMember member = familyMemberMapper.selectById(memberId);
  338. if (member == null) {
  339. throw new RuntimeException("家庭成员不存在");
  340. }
  341. // 归属校验:成员必须属于当前用户家庭
  342. Long myFamilyId = me.getFamilyId();
  343. if (myFamilyId == null || !myFamilyId.equals(member.getFamilyId())) {
  344. throw new RuntimeException("该成员不属于您的家庭");
  345. }
  346. for (String field : MEMBER_BACKFILL_WHITELIST) {
  347. String value = dto.getFields().get(field);
  348. if (value == null || value.isEmpty()) continue;
  349. applyMemberBackfillField(member, field, value.trim());
  350. updatedFields.add(field);
  351. }
  352. familyMemberMapper.updateById(member);
  353. profile = member;
  354. } else {
  355. throw new RuntimeException("targetType 仅支持 user/member");
  356. }
  357. Map<String, Object> result = new HashMap<>();
  358. result.put("updatedFields", updatedFields);
  359. result.put("profile", profile);
  360. return result;
  361. }
  362. private void applyUserBackfillField(User user, String field, String value) {
  363. switch (field) {
  364. case "phone":
  365. if (!value.matches("^1[3-9]\\d{9}$")) throw new RuntimeException("手机号格式不正确");
  366. user.setPhone(value);
  367. break;
  368. case "gender":
  369. if (!"male".equals(value) && !"female".equals(value)) throw new RuntimeException("性别取值非法");
  370. user.setGender(value);
  371. break;
  372. case "birthday":
  373. try {
  374. java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
  375. user.setBirthday(sdf.parse(value));
  376. } catch (Exception e) {
  377. throw new RuntimeException("生日格式不正确,应为 yyyy-MM-dd");
  378. }
  379. break;
  380. case "birthHour":
  381. if (!"子丑寅卯辰巳午未申酉戌亥".contains(value)) throw new RuntimeException("出生时辰取值非法");
  382. user.setBirthHour(value);
  383. break;
  384. case "dietPreferences":
  385. user.setDietPreferences(value);
  386. break;
  387. case "address":
  388. user.setAddress(value);
  389. break;
  390. default:
  391. break;
  392. }
  393. }
  394. private void applyMemberBackfillField(FamilyMember member, String field, String value) {
  395. switch (field) {
  396. case "phone":
  397. if (!value.matches("^1[3-9]\\d{9}$")) throw new RuntimeException("手机号格式不正确");
  398. member.setPhone(value);
  399. break;
  400. case "gender":
  401. if (!"male".equals(value) && !"female".equals(value)) throw new RuntimeException("性别取值非法");
  402. member.setGender(value);
  403. break;
  404. case "birthday":
  405. try {
  406. java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
  407. member.setBirthday(sdf.parse(value));
  408. } catch (Exception e) {
  409. throw new RuntimeException("生日格式不正确,应为 yyyy-MM-dd");
  410. }
  411. break;
  412. case "birthHour":
  413. if (!"子丑寅卯辰巳午未申酉戌亥".contains(value)) throw new RuntimeException("出生时辰取值非法");
  414. member.setBirthHour(value);
  415. break;
  416. default:
  417. break;
  418. }
  419. }
  420. /**
  421. * 检查手机号是否已注册
  422. * @param phone 手机号
  423. * @return true=已注册,false=未注册
  424. */
  425. public boolean isPhoneRegistered(String phone) {
  426. if (phone == null || phone.isEmpty()) {
  427. return false;
  428. }
  429. User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
  430. .eq(User::getPhone, phone));
  431. return user != null;
  432. }
  433. /**
  434. * 直接注册(测试环境:不验证验证码)
  435. */
  436. public LoginResultDTO directRegister(DirectRegisterDTO dto) {
  437. // 创建用户(家庭待首次使用家庭功能时创建,或通过邀请码加入)
  438. User user = new User();
  439. user.setOpenid("phone:" + dto.getPhone());
  440. user.setUnionid("");
  441. user.setRole(dto.getRole() != null ? dto.getRole() : "parent");
  442. user.setNickname(dto.getNickname() != null ? dto.getNickname() : "用户" + dto.getPhone().substring(7));
  443. user.setAvatar(dto.getAvatar());
  444. user.setPhone(dto.getPhone());
  445. user.setBirthday(dto.getBirthday());
  446. user.setGender(dto.getGender());
  447. user.setPassword("");
  448. user.setCreatedAt(new Date());
  449. user.setUpdatedAt(new Date());
  450. createUserAndFamily(user);
  451. // 如果有邀请码,提交加入家庭请求(需管理员审批)
  452. if (dto.getInviteCode() != null && !dto.getInviteCode().isEmpty()) {
  453. try {
  454. familyJoinRequestService.createJoinRequest(user.getId(), dto.getInviteCode());
  455. } catch (Exception e) {
  456. // 忽略邀请码错误
  457. }
  458. }
  459. // 生成token
  460. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  461. LoginResultDTO result = new LoginResultDTO();
  462. result.setToken(token);
  463. result.setUserId(user.getId());
  464. result.setRole(user.getRole());
  465. result.setRoles(user.getRoles());
  466. result.setFamilyId(user.getFamilyId());
  467. result.setNickname(user.getNickname());
  468. result.setIsNewUser(true);
  469. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  470. populateTeacherFields(result, user);
  471. return result;
  472. }
  473. @Deprecated
  474. public String switchMode(Long userId, String mode) {
  475. log.warn("已废弃的 switchMode 被调用, userId={}, mode={} - 应使用 /api/family/member/switch", userId, mode);
  476. User user = userMapper.selectById(userId);
  477. if (user == null) {
  478. return null;
  479. }
  480. // 管理员角色不允许切换
  481. if ("admin".equals(user.getRole())) {
  482. throw new RuntimeException("管理员角色不允许切换");
  483. }
  484. // 验证用户是否拥有该角色
  485. if (!hasRole(user, mode)) {
  486. return null;
  487. }
  488. user.setRole(mode);
  489. user.setUpdatedAt(new Date());
  490. userMapper.updateById(user);
  491. return mode;
  492. }
  493. /**
  494. * 切换用户当前角色 (支持 parent/child/teacher)
  495. * 用户需要先拥有该角色才能切换
  496. */
  497. public String switchRole(Long userId, String targetRole) {
  498. User user = userMapper.selectById(userId);
  499. if (user == null) {
  500. return null;
  501. }
  502. // 管理员角色不允许切换
  503. if ("admin".equals(user.getRole())) {
  504. throw new RuntimeException("管理员角色不允许切换");
  505. }
  506. if (!hasRole(user, targetRole)) {
  507. throw new RuntimeException("您没有该角色权限");
  508. }
  509. if (!isValidRole(targetRole)) {
  510. return null;
  511. }
  512. user.setRole(targetRole);
  513. user.setUpdatedAt(new Date());
  514. userMapper.updateById(user);
  515. return targetRole;
  516. }
  517. public LoginResultDTO switchRoleForLogin(Long userId, String role) {
  518. User user = userMapper.selectById(userId);
  519. if (user == null) {
  520. throw new RuntimeException("用户不存在");
  521. }
  522. // 管理员角色不允许切换
  523. if ("admin".equals(user.getRole())) {
  524. throw new RuntimeException("管理员角色不允许切换");
  525. }
  526. if ("teacher".equals(role) && !hasRole(user, "teacher")) {
  527. throw new RuntimeException("您不是成长规划师,无法切换为规划师模式");
  528. }
  529. user.setRole(role);
  530. user.setUpdatedAt(new Date());
  531. userMapper.updateById(user);
  532. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  533. LoginResultDTO result = new LoginResultDTO();
  534. result.setToken(token);
  535. result.setUserId(user.getId());
  536. result.setRole(user.getRole());
  537. result.setRoles(user.getRoles());
  538. result.setFamilyId(user.getFamilyId());
  539. result.setNickname(user.getNickname());
  540. result.setIsNewUser(false);
  541. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  542. populateTeacherFields(result, user);
  543. return result;
  544. }
  545. /**
  546. * 获取用户所有角色列表
  547. */
  548. public List<String> getUserRoles(Long userId) {
  549. User user = userMapper.selectById(userId);
  550. if (user == null) {
  551. return new ArrayList<>();
  552. }
  553. return parseRoles(user.getRoles());
  554. }
  555. /**
  556. * 检查用户是否拥有指定角色
  557. */
  558. public boolean hasRole(Long userId, String role) {
  559. User user = userMapper.selectById(userId);
  560. if (user == null) {
  561. return false;
  562. }
  563. return hasRole(user, role);
  564. }
  565. /**
  566. * 添加角色到用户 (用于用户注册成为老师时)
  567. */
  568. public boolean addRole(Long userId, String newRole) {
  569. User user = userMapper.selectById(userId);
  570. if (user == null || !isValidRole(newRole)) {
  571. return false;
  572. }
  573. // 如果用户还没有roles字段,设置默认值
  574. if (user.getRoles() == null || user.getRoles().isEmpty()) {
  575. user.setRoles(user.getRole()); // 当前角色作为第一个角色
  576. }
  577. // 检查是否已拥有该角色
  578. if (hasRole(user, newRole)) {
  579. return true; // 已有该角色
  580. }
  581. // 添加新角色
  582. List<String> roleList = parseRoles(user.getRoles());
  583. roleList.add(newRole);
  584. user.setRoles(joinRoles(roleList));
  585. user.setUpdatedAt(new Date());
  586. userMapper.updateById(user);
  587. return true;
  588. }
  589. /**
  590. * 检查用户是否拥有指定角色 (内部方法)
  591. */
  592. private boolean hasRole(User user, String role) {
  593. List<String> roles = parseRoles(user.getRoles());
  594. return roles.contains(role);
  595. }
  596. /**
  597. * 解析角色字符串为列表
  598. */
  599. public List<String> parseRoles(String rolesStr) {
  600. List<String> result = new ArrayList<>();
  601. if (rolesStr == null || rolesStr.isEmpty()) {
  602. return result;
  603. }
  604. // 支持JSON数组格式或逗号分隔格式
  605. if (rolesStr.startsWith("[")) {
  606. // JSON格式解析
  607. rolesStr = rolesStr.replace("[", "").replace("]", "")
  608. .replace("\"", "").replace(" ", "");
  609. String[] arr = rolesStr.split(",");
  610. for (String r : arr) {
  611. if (!r.isEmpty()) {
  612. result.add(r.trim());
  613. }
  614. }
  615. } else {
  616. // 逗号分隔格式
  617. String[] arr = rolesStr.split(",");
  618. for (String r : arr) {
  619. if (!r.trim().isEmpty()) {
  620. result.add(r.trim());
  621. }
  622. }
  623. }
  624. return result;
  625. }
  626. /**
  627. * 将角色列表转换为字符串
  628. */
  629. private String joinRoles(List<String> roles) {
  630. return String.join(",", roles);
  631. }
  632. /**
  633. * 检查角色是否有效
  634. */
  635. private boolean isValidRole(String role) {
  636. return "parent".equals(role) || "child".equals(role) || "teacher".equals(role)
  637. || "supplier_admin".equals(role);
  638. }
  639. /**
  640. * 家长切换到指定孩子身份(视图切换,不修改用户的实际角色)
  641. * 家长可以在家长模式下查看孩子的任务和积分
  642. */
  643. @Deprecated
  644. public Long switchToChild(Long userId, Long memberId) {
  645. log.warn("已废弃的 switchToChild 被调用, userId={}, memberId={} - 应使用 /api/family/member/switch", userId, memberId);
  646. User user = userMapper.selectById(userId);
  647. if (user == null) {
  648. return null;
  649. }
  650. FamilyMember child = familyMemberMapper.selectById(memberId);
  651. if (child == null || !child.getFamilyId().equals(user.getFamilyId())) {
  652. return null;
  653. }
  654. // 注意:不修改用户的 role 字段,只是返回 memberId 让前端切换视图
  655. // 前端应该使用 currentRole 或 viewMode 来控制显示模式
  656. return memberId;
  657. }
  658. /**
  659. * 从孩子视图切换回家长视图(旧版,无密码校验)
  660. * 保留以兼容旧 JWT token
  661. */
  662. public boolean switchBackToParent(Long userId) {
  663. log.warn("无密码的 switchBackToParent 被调用, userId={} - 建议前端使用 /switch-back-verify", userId);
  664. User user = userMapper.selectById(userId);
  665. if (user == null) {
  666. return false;
  667. }
  668. return true;
  669. }
  670. /**
  671. * 带密码校验的切换回家长视图
  672. * @param userId 用户ID
  673. * @param password 密码
  674. * @return 校验并切换成功返回 true
  675. */
  676. public boolean verifySwitchBack(Long userId, String password) {
  677. if (password == null || password.isEmpty()) {
  678. return false;
  679. }
  680. if (!verifyPassword(userId, password)) {
  681. return false;
  682. }
  683. return true;
  684. }
  685. /**
  686. * 获取对家庭成员可见的成员列表(家长+孩子)
  687. */
  688. public List<FamilyMemberVO> getVisibleMembers(Long userId) {
  689. User user = userMapper.selectById(userId);
  690. if (user == null) {
  691. return new ArrayList<>();
  692. }
  693. if (user.getFamilyId() == null) {
  694. // 兜底:无家庭时自动创建,避免"暂无家庭"空态
  695. ensureUserHasFamily(user);
  696. }
  697. Long familyId = user.getFamilyId();
  698. List<FamilyMemberVO> result = new ArrayList<>();
  699. Family family = familyMapper.selectById(familyId);
  700. Long creatorId = (family != null) ? family.getCreatorId() : null;
  701. boolean isAdmin = creatorId != null && creatorId.equals(userId);
  702. // 家长:show_to_family=1 的同家庭家长(自己始终可见)
  703. List<User> parents = userMapper.selectList(new LambdaQueryWrapper<User>()
  704. .eq(User::getFamilyId, familyId)
  705. .eq(User::getRole, "parent"));
  706. for (User p : parents) {
  707. if (p.getId().equals(userId) || (p.getShowToFamily() != null && p.getShowToFamily() == 1)) {
  708. FamilyMemberVO vo = new FamilyMemberVO();
  709. vo.setId(p.getId());
  710. vo.setSource("parent");
  711. vo.setUserId(p.getId());
  712. vo.setNickname(p.getNickname());
  713. vo.setAvatar(p.getAvatar());
  714. vo.setPhone(p.getPhone());
  715. vo.setGender(p.getGender());
  716. vo.setEffectiveRole("parent");
  717. vo.setMemberType("parent");
  718. vo.setRoleLabel(p.getFamilyRole() != null ? p.getFamilyRole() : "家长");
  719. vo.setTotalPoints(p.getTotalPoints());
  720. vo.setShowToFamily(p.getShowToFamily() != null ? p.getShowToFamily() : 1);
  721. vo.setIsAdmin(isAdmin);
  722. vo.setIsSelf(p.getId().equals(userId));
  723. result.add(vo);
  724. }
  725. }
  726. // 孩子/其他成员:复用 FamilyMemberService.listMembers 的完整字段计算(关系标签/辈分/配偶/信任分等),
  727. // 再按 show_to_family 过滤(自己始终可见)
  728. List<FamilyMemberVO> members = familyMemberService.listMembers(userId);
  729. for (FamilyMemberVO vo : members) {
  730. boolean isSelf = vo.getUserId() != null && vo.getUserId().equals(userId);
  731. if (isSelf || vo.getShowToFamily() == null || vo.getShowToFamily() == 1) {
  732. vo.setIsSelf(isSelf);
  733. result.add(vo);
  734. }
  735. }
  736. return result;
  737. }
  738. /**
  739. * 更新成员可见性
  740. */
  741. public boolean updateVisibility(Long userId, Long memberId, String memberType, Integer showToFamily) {
  742. User user = userMapper.selectById(userId);
  743. if (user == null) {
  744. return false;
  745. }
  746. if ("child".equals(memberType)) {
  747. FamilyMember child = familyMemberMapper.selectById(memberId);
  748. if (child == null || !child.getFamilyId().equals(user.getFamilyId())) {
  749. return false;
  750. }
  751. child.setShowToFamily(showToFamily);
  752. child.setUpdatedAt(new Date());
  753. familyMemberMapper.updateById(child);
  754. return true;
  755. } else if ("parent".equals(memberType)) {
  756. // 家长只能改自己的可见性
  757. if (!memberId.equals(userId)) {
  758. return false;
  759. }
  760. user.setShowToFamily(showToFamily);
  761. user.setUpdatedAt(new Date());
  762. userMapper.updateById(user);
  763. return true;
  764. }
  765. return false;
  766. }
  767. public LoginResultDTO registerWithIdCard(RegisterWithIdCardDTO dto) {
  768. // 验证验证码
  769. boolean valid = verificationCodeService.verifyCode(dto.getPhone(), dto.getCode(), "login");
  770. if (!valid) {
  771. throw new RuntimeException("验证码错误或已过期");
  772. }
  773. // 通过身份证查找是否已有注册的用户
  774. User existingUser = userMapper.selectOne(new LambdaQueryWrapper<User>()
  775. .eq(User::getIdCard, dto.getIdCard()));
  776. User user;
  777. if (existingUser != null) {
  778. // 管理员不能登录小程序
  779. if ("admin".equals(existingUser.getRole())) {
  780. throw new RuntimeException("该账号不允许登录小程序");
  781. }
  782. // 如果存在相同身份证的用户,沿用其已有信息
  783. user = existingUser;
  784. // 更新用户信息(昵称和头像可以更新)
  785. user.setNickname(dto.getNickname());
  786. user.setAvatar(dto.getAvatar());
  787. // 手机号也可以更新(如果需要)
  788. user.setOpenid("phone:" + dto.getPhone());
  789. user.setUpdatedAt(new Date());
  790. userMapper.updateById(user);
  791. } else {
  792. // 新用户,仅创建用户(家庭待首次使用家庭功能时创建)
  793. user = new User();
  794. user.setOpenid("phone:" + dto.getPhone());
  795. user.setUnionid("");
  796. user.setRole("parent");
  797. user.setNickname(dto.getNickname());
  798. user.setAvatar(dto.getAvatar());
  799. user.setIdCard(dto.getIdCard());
  800. user.setRealName(dto.getRealName());
  801. user.setPassword(""); // 初始密码为空,需要后续设置
  802. user.setCreatedAt(new Date());
  803. user.setUpdatedAt(new Date());
  804. createUserAndFamily(user);
  805. }
  806. ensureUserHasFamily(user);
  807. // 生成token
  808. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  809. LoginResultDTO result = new LoginResultDTO();
  810. result.setToken(token);
  811. result.setUserId(user.getId());
  812. result.setRole(user.getRole());
  813. result.setFamilyId(user.getFamilyId());
  814. result.setNickname(user.getNickname());
  815. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  816. populateTeacherFields(result, user);
  817. return result;
  818. }
  819. public ChildInfoDTO createChild(Long userId, CreateChildDTO dto) {
  820. User user = userMapper.selectById(userId);
  821. if (user == null) {
  822. return null;
  823. }
  824. if (dto.getNickname() == null || dto.getNickname().trim().isEmpty()) {
  825. throw new IllegalArgumentException("孩子昵称不能为空");
  826. }
  827. if (dto.getAge() == null || dto.getAge() < 1 || dto.getAge() > 18) {
  828. throw new IllegalArgumentException("请输入有效的年龄(1-18岁)");
  829. }
  830. FamilyMember child = new FamilyMember();
  831. child.setUserId(userId);
  832. child.setFamilyId(user.getFamilyId());
  833. child.setNickname(dto.getNickname().trim());
  834. child.setAge(dto.getAge());
  835. child.setBirthday(dto.getBirthday());
  836. child.setGender(dto.getGender());
  837. child.setPhone(dto.getPhone());
  838. child.setIdCard(dto.getIdCard());
  839. // 扣分开关:优先使用传入值,否则根据年龄判断
  840. if (dto.getPenaltyEnabled() != null) {
  841. child.setPenaltyEnabled(dto.getPenaltyEnabled());
  842. } else {
  843. // 年龄可能为空,默认不扣分
  844. Integer age = dto.getAge();
  845. child.setPenaltyEnabled(age != null && age >= 6 ? 1 : 0);
  846. }
  847. child.setTheme("default");
  848. child.setTotalPoints(0);
  849. child.setStreakDays(0);
  850. child.setFocusMaxDaily(3);
  851. child.setFocusRemaining(3);
  852. child.setFocusResetDate(new Date());
  853. child.setCreatedAt(new Date());
  854. child.setUpdatedAt(new Date());
  855. familyMemberMapper.insert(child);
  856. return toChildInfoDTO(child);
  857. }
  858. public List<ChildInfoDTO> getChildren(Long userId) {
  859. User user = userMapper.selectById(userId);
  860. if (user == null) {
  861. return null;
  862. }
  863. List<FamilyMember> children = familyMemberMapper.selectList(new LambdaQueryWrapper<FamilyMember>()
  864. .eq(FamilyMember::getFamilyId, user.getFamilyId()));
  865. return children.stream().map(this::toChildInfoDTO).collect(java.util.stream.Collectors.toList());
  866. }
  867. public boolean updateChild(Long userId, Long memberId, UpdateChildDTO dto) {
  868. User user = userMapper.selectById(userId);
  869. if (user == null) {
  870. return false;
  871. }
  872. FamilyMember child = familyMemberMapper.selectById(memberId);
  873. if (child == null || child.getFamilyId() == null || !child.getFamilyId().equals(user.getFamilyId())) {
  874. return false;
  875. }
  876. if (dto.getNickname() != null) {
  877. child.setNickname(dto.getNickname());
  878. }
  879. if (dto.getAge() != null) {
  880. child.setAge(dto.getAge());
  881. }
  882. if (dto.getBirthday() != null) {
  883. child.setBirthday(dto.getBirthday());
  884. }
  885. if (dto.getGender() != null) {
  886. child.setGender(dto.getGender());
  887. }
  888. if (dto.getPhone() != null) {
  889. child.setPhone(dto.getPhone());
  890. }
  891. if (dto.getIdCard() != null) {
  892. child.setIdCard(dto.getIdCard());
  893. }
  894. if (dto.getPenaltyEnabled() != null) {
  895. child.setPenaltyEnabled(dto.getPenaltyEnabled());
  896. }
  897. if (dto.getTheme() != null) {
  898. child.setTheme(dto.getTheme());
  899. }
  900. if (dto.getShowToFamily() != null) {
  901. child.setShowToFamily(dto.getShowToFamily());
  902. }
  903. child.setUpdatedAt(new Date());
  904. familyMemberMapper.updateById(child);
  905. return true;
  906. }
  907. private static final BCryptPasswordEncoder BC_PASSWORD_ENCODER = new BCryptPasswordEncoder();
  908. private String encryptPassword(String password) {
  909. return BC_PASSWORD_ENCODER.encode(password);
  910. }
  911. private boolean verifyPasswordHash(String password, String storedHash) {
  912. if (storedHash == null) {
  913. return false;
  914. }
  915. if (storedHash.startsWith("$2a$") || storedHash.startsWith("$2b$")) {
  916. return BC_PASSWORD_ENCODER.matches(password, storedHash);
  917. }
  918. // 旧 MD5 哈希:验证通过后自动升级
  919. String md5 = DigestUtils.md5DigestAsHex(("xzyj_salt_" + password).getBytes(StandardCharsets.UTF_8));
  920. return storedHash.equals(md5);
  921. }
  922. private String generateInviteCode() {
  923. return UUID.randomUUID().toString().substring(0, 8).toUpperCase();
  924. }
  925. private ChildInfoDTO toChildInfoDTO(FamilyMember child) {
  926. ChildInfoDTO dto = new ChildInfoDTO();
  927. dto.setId(child.getId());
  928. dto.setNickname(child.getNickname());
  929. dto.setAge(child.getAge());
  930. dto.setBirthday(child.getBirthday());
  931. dto.setGender(child.getGender());
  932. dto.setPhone(child.getPhone());
  933. dto.setIdCard(child.getIdCard());
  934. dto.setPenaltyEnabled(child.getPenaltyEnabled());
  935. dto.setTheme(child.getTheme());
  936. dto.setTotalPoints(child.getTotalPoints());
  937. dto.setSystemPoints(child.getSystemPoints());
  938. dto.setStreakDays(child.getStreakDays());
  939. dto.setFocusRemaining(child.getFocusRemaining());
  940. dto.setShowToFamily(child.getShowToFamily());
  941. return dto;
  942. }
  943. /**
  944. * 更新用户信息
  945. */
  946. public boolean updateUserInfo(Long userId, UpdateUserDTO dto) {
  947. User user = userMapper.selectById(userId);
  948. if (user == null) {
  949. return false;
  950. }
  951. // 管理员角色不允许修改
  952. if ("admin".equals(user.getRole())) {
  953. throw new RuntimeException("管理员角色不允许修改");
  954. }
  955. if (dto.getNickname() != null) {
  956. user.setNickname(dto.getNickname());
  957. }
  958. if (dto.getAvatar() != null) {
  959. user.setAvatar(dto.getAvatar());
  960. }
  961. if (dto.getRealName() != null) {
  962. user.setRealName(dto.getRealName());
  963. }
  964. // 更新角色
  965. if (dto.getRole() != null) {
  966. user.setRole(dto.getRole());
  967. }
  968. // 更新性别
  969. if (dto.getGender() != null) {
  970. String gender = dto.getGender();
  971. if ("男".equals(gender) || "0".equals(gender)) {
  972. gender = "male";
  973. } else if ("女".equals(gender) || "1".equals(gender)) {
  974. gender = "female";
  975. }
  976. if (!"male".equals(gender) && !"female".equals(gender)) {
  977. throw new RuntimeException("性别取值非法,仅支持 male/female");
  978. }
  979. user.setGender(gender);
  980. }
  981. // 更新生日(将字符串转换为Date)
  982. if (dto.getBirthday() != null && !dto.getBirthday().isEmpty()) {
  983. try {
  984. java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
  985. user.setBirthday(sdf.parse(dto.getBirthday()));
  986. } catch (Exception e) {
  987. // 忽略解析错误
  988. }
  989. }
  990. // 更新身份证号
  991. if (dto.getIdCard() != null && !dto.getIdCard().isEmpty()) {
  992. user.setIdCard(dto.getIdCard());
  993. // 如果没有手动设置生日,尝试从身份证号提取
  994. if (user.getBirthday() == null) {
  995. String birthdayFromIdCard = extractBirthdayFromIdCard(dto.getIdCard());
  996. if (birthdayFromIdCard != null) {
  997. try {
  998. java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
  999. user.setBirthday(sdf.parse(birthdayFromIdCard));
  1000. } catch (Exception e) {
  1001. // 忽略解析错误
  1002. }
  1003. }
  1004. }
  1005. }
  1006. // 更新成长规划师信息
  1007. if (dto.getTeacherNo() != null) {
  1008. user.setTeacherNo(dto.getTeacherNo());
  1009. }
  1010. if (dto.getTeacherPhoto() != null) {
  1011. user.setTeacherPhoto(dto.getTeacherPhoto());
  1012. }
  1013. if (dto.getCertificateImage() != null) {
  1014. user.setCertificateImage(dto.getCertificateImage());
  1015. }
  1016. // 更新个人信息补充字段
  1017. if (dto.getEthnicity() != null) {
  1018. user.setEthnicity(dto.getEthnicity());
  1019. }
  1020. if (dto.getBloodType() != null) {
  1021. user.setBloodType(dto.getBloodType());
  1022. }
  1023. if (dto.getHighestEducation() != null) {
  1024. user.setHighestEducation(dto.getHighestEducation());
  1025. }
  1026. if (dto.getMaritalStatus() != null) {
  1027. user.setMaritalStatus(dto.getMaritalStatus());
  1028. }
  1029. if (dto.getFamilyRole() != null) {
  1030. user.setFamilyRole(dto.getFamilyRole());
  1031. }
  1032. // 更新AI助手形象
  1033. if (dto.getMascot() != null) {
  1034. user.setMascot(dto.getMascot());
  1035. }
  1036. // 更新兴趣爱好
  1037. if (dto.getHobbies() != null) {
  1038. String oldHobbies = user.getHobbies();
  1039. user.setHobbies(dto.getHobbies());
  1040. recordProfileChange(userId, "hobbies", oldHobbies, dto.getHobbies(), "user_edit");
  1041. }
  1042. // 更新饮食偏好
  1043. if (dto.getDietPreferences() != null) {
  1044. String oldDiet = user.getDietPreferences();
  1045. user.setDietPreferences(dto.getDietPreferences());
  1046. recordProfileChange(userId, "dietPreferences", oldDiet, dto.getDietPreferences(), "user_edit");
  1047. }
  1048. // 更新地址
  1049. if (dto.getAddress() != null) {
  1050. user.setAddress(dto.getAddress());
  1051. }
  1052. userMapper.updateById(user);
  1053. return true;
  1054. }
  1055. /**
  1056. * 记录用户信息变更历史(仅当新旧值不同时写入)
  1057. */
  1058. private void recordProfileChange(Long userId, String fieldName,
  1059. String oldValue, String newValue, String source) {
  1060. if (oldValue == null && newValue == null) return;
  1061. if (oldValue != null && oldValue.equals(newValue)) return;
  1062. if (newValue != null && newValue.equals(oldValue)) return;
  1063. UserProfileHistory history = new UserProfileHistory();
  1064. history.setUserId(userId);
  1065. history.setFieldName(fieldName);
  1066. history.setOldValue(oldValue);
  1067. history.setNewValue(newValue);
  1068. history.setSource(source);
  1069. history.setChangedAt(new Date());
  1070. history.setCreatedAt(new Date());
  1071. userProfileHistoryMapper.insert(history);
  1072. }
  1073. /**
  1074. * 从身份证号提取生日
  1075. * 15位:1985年07月08日 -> 1985-07-08
  1076. * 18位:1985年07月08日 -> 1985-07-08
  1077. */
  1078. private String extractBirthdayFromIdCard(String idCard) {
  1079. if (idCard == null || idCard.isEmpty()) {
  1080. return null;
  1081. }
  1082. try {
  1083. if (idCard.length() == 15) {
  1084. // 15位身份证:出生日期在第6-12位
  1085. String year = "19" + idCard.substring(6, 8);
  1086. String month = idCard.substring(8, 10);
  1087. String day = idCard.substring(10, 12);
  1088. return String.format("%s-%s-%s", year, month, day);
  1089. } else if (idCard.length() == 18) {
  1090. // 18位身份证:出生日期在第6-14位
  1091. String year = idCard.substring(6, 10);
  1092. String month = idCard.substring(10, 12);
  1093. String day = idCard.substring(12, 14);
  1094. return String.format("%s-%s-%s", year, month, day);
  1095. }
  1096. } catch (Exception e) {
  1097. // 忽略解析异常
  1098. }
  1099. return null;
  1100. }
  1101. /**
  1102. * 通过邀请码加入家庭
  1103. */
  1104. public boolean joinFamily(Long userId, String inviteCode) {
  1105. if (inviteCode == null || inviteCode.isEmpty()) {
  1106. throw new RuntimeException("邀请码不能为空");
  1107. }
  1108. // 查找家庭
  1109. Family family = familyMapper.selectOne(new LambdaQueryWrapper<Family>()
  1110. .eq(Family::getInviteCode, inviteCode));
  1111. if (family == null) {
  1112. throw new RuntimeException("邀请码无效");
  1113. }
  1114. // 更新用户的家庭ID
  1115. User user = userMapper.selectById(userId);
  1116. if (user == null) {
  1117. throw new RuntimeException("用户不存在");
  1118. }
  1119. if (user.getFamilyId() != null && !user.getFamilyId().equals(family.getId())) {
  1120. familyInvitationService.mergeIntoFamily(userId, family.getId());
  1121. } else if (user.getFamilyId() == null) {
  1122. user.setFamilyId(family.getId());
  1123. user.setUpdatedAt(new Date());
  1124. userMapper.updateById(user);
  1125. }
  1126. // 如果家庭绑定了成长规划师,自动将家庭ID添加到成长规划师的 teacherFamilyIds
  1127. if (family.getTeacherId() != null) {
  1128. User teacher = userMapper.selectById(family.getTeacherId());
  1129. if (teacher != null && "teacher".equals(teacher.getRole())) {
  1130. String currentIds = teacher.getTeacherFamilyIds();
  1131. String newFamilyId = String.valueOf(family.getId());
  1132. if (currentIds == null || !currentIds.contains(newFamilyId)) {
  1133. String updatedIds = currentIds == null ? newFamilyId : currentIds + "," + newFamilyId;
  1134. teacher.setTeacherFamilyIds(updatedIds);
  1135. teacher.setUpdatedAt(new Date());
  1136. userMapper.updateById(teacher);
  1137. }
  1138. }
  1139. }
  1140. return true;
  1141. }
  1142. /**
  1143. * 创建用户并自动创建家庭(注册时用)
  1144. * @return 创建后的 user(含 familyId 和 isFamilyAdmin)
  1145. */
  1146. private User createUserAndFamily(User user) {
  1147. // 1. 插入 user
  1148. userMapper.insert(user);
  1149. // 2. 创建家庭
  1150. Family family = new Family();
  1151. String familyName = (user.getNickname() != null && !user.getNickname().isEmpty())
  1152. ? user.getNickname() + "的家庭" : "我的家庭";
  1153. family.setName(familyName);
  1154. family.setInviteCode(generateInviteCode());
  1155. family.setCreatorId(user.getId());
  1156. family.setCreatedAt(new Date());
  1157. family.setUpdatedAt(new Date());
  1158. familyMapper.insert(family);
  1159. // 3. 设置 user 的家庭关联
  1160. user.setFamilyId(family.getId());
  1161. user.setIsFamilyAdmin(true);
  1162. user.setUpdatedAt(new Date());
  1163. userMapper.updateById(user);
  1164. log.info("已为用户 {} 自动创建家庭 {} ({})", user.getId(), family.getId(), familyName);
  1165. return user;
  1166. }
  1167. /**
  1168. * 确保用户有家庭(安全兜底:注册/登录时如果 familyId 为空则自动创建)
  1169. */
  1170. private void ensureUserHasFamily(User user) {
  1171. if (user.getFamilyId() != null && user.getFamilyId() != 0L) {
  1172. return; // 已有家庭
  1173. }
  1174. Family family = new Family();
  1175. String familyName = (user.getNickname() != null && !user.getNickname().isEmpty())
  1176. ? user.getNickname() + "的家庭" : "我的家庭";
  1177. family.setName(familyName);
  1178. family.setInviteCode(generateInviteCode());
  1179. family.setCreatorId(user.getId());
  1180. family.setCreatedAt(new Date());
  1181. family.setUpdatedAt(new Date());
  1182. familyMapper.insert(family);
  1183. user.setFamilyId(family.getId());
  1184. user.setIsFamilyAdmin(true);
  1185. user.setUpdatedAt(new Date());
  1186. userMapper.updateById(user);
  1187. log.info("安全兜底:为用户 {} 自动创建家庭 {}", user.getId(), family.getId());
  1188. }
  1189. /**
  1190. * 确保用户有家庭(公开兜底入口,供查询家庭类接口在 familyId 为空时调用)
  1191. * @return 用户的 familyId;用户不存在时返回 null
  1192. */
  1193. public Long ensureUserFamily(Long userId) {
  1194. User user = userMapper.selectById(userId);
  1195. if (user == null) {
  1196. return null;
  1197. }
  1198. ensureUserHasFamily(user);
  1199. return user.getFamilyId();
  1200. }
  1201. /**
  1202. * 为用户创建新家庭
  1203. */
  1204. public Long createFamilyForUser(Long userId, String familyName) {
  1205. User user = userMapper.selectById(userId);
  1206. if (user == null) {
  1207. throw new RuntimeException("用户不存在");
  1208. }
  1209. // 创建新家庭
  1210. Family family = new Family();
  1211. family.setName(familyName);
  1212. family.setInviteCode(generateInviteCode());
  1213. family.setCreatedAt(new Date());
  1214. family.setUpdatedAt(new Date());
  1215. familyMapper.insert(family);
  1216. // 更新用户的家庭ID
  1217. user.setFamilyId(family.getId());
  1218. user.setUpdatedAt(new Date());
  1219. userMapper.updateById(user);
  1220. return family.getId();
  1221. }
  1222. /**
  1223. * 使用邀请码注册并加入家庭
  1224. */
  1225. public LoginResultDTO registerWithInviteCode(RegisterWithInviteCodeDTO dto) {
  1226. String phone = dto.getPhone();
  1227. String inviteCode = dto.getInviteCode();
  1228. String role = dto.getRole() != null ? dto.getRole() : "parent";
  1229. String nickname = dto.getNickname() != null ? dto.getNickname() : "用户" + phone.substring(7);
  1230. Result<DistributionRelation> distResult = distributionService.findByInviteCode(inviteCode);
  1231. if (distResult.getCode() == 200 && distResult.getData() != null) {
  1232. User user = new User();
  1233. user.setOpenid("phone:" + phone);
  1234. user.setUnionid("");
  1235. user.setRole(role);
  1236. user.setNickname(nickname);
  1237. user.setPhone(phone);
  1238. user.setPassword("");
  1239. user.setCreatedAt(new Date());
  1240. user.setUpdatedAt(new Date());
  1241. createUserAndFamily(user);
  1242. distributionService.registerByInviteCode(inviteCode, user.getId());
  1243. ensureUserHasFamily(user);
  1244. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  1245. LoginResultDTO result = new LoginResultDTO();
  1246. result.setToken(token);
  1247. result.setUserId(user.getId());
  1248. result.setRole(user.getRole());
  1249. result.setRoles(user.getRoles());
  1250. result.setFamilyId(user.getFamilyId());
  1251. result.setNickname(user.getNickname());
  1252. result.setIsNewUser(true);
  1253. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  1254. populateTeacherFields(result, user);
  1255. return result;
  1256. }
  1257. Family family = familyMapper.selectOne(new LambdaQueryWrapper<Family>()
  1258. .eq(Family::getInviteCode, inviteCode));
  1259. if (family == null) {
  1260. throw new RuntimeException("邀请码无效");
  1261. }
  1262. User user;
  1263. boolean isNewUser = false;
  1264. // 检查手机号是否已注册
  1265. User existingUser = userMapper.selectOne(new LambdaQueryWrapper<User>()
  1266. .eq(User::getPhone, phone));
  1267. if (existingUser != null) {
  1268. // 管理员不能登录小程序
  1269. if ("admin".equals(existingUser.getRole())) {
  1270. throw new RuntimeException("该账号不允许登录小程序");
  1271. }
  1272. user = existingUser;
  1273. ensureUserHasFamily(user);
  1274. try {
  1275. familyJoinRequestService.createJoinRequest(user.getId(), inviteCode);
  1276. log.info("已为用户 {} 提交加入家庭 {} 的请求", user.getId(), family.getId());
  1277. } catch (Exception e) {
  1278. log.warn("提交加入家庭请求失败: {}", e.getMessage());
  1279. }
  1280. } else {
  1281. isNewUser = true;
  1282. user = new User();
  1283. user.setOpenid("phone:" + phone);
  1284. user.setUnionid("");
  1285. user.setRole(role);
  1286. user.setNickname(nickname);
  1287. user.setPhone(phone);
  1288. user.setPassword("");
  1289. user.setCreatedAt(new Date());
  1290. user.setUpdatedAt(new Date());
  1291. createUserAndFamily(user);
  1292. try {
  1293. familyJoinRequestService.createJoinRequest(user.getId(), inviteCode);
  1294. log.info("已为用户 {} 提交加入家庭 {} 的请求", user.getId(), family.getId());
  1295. } catch (Exception e) {
  1296. log.warn("提交加入家庭请求失败: {}", e.getMessage());
  1297. }
  1298. }
  1299. ensureUserHasFamily(user);
  1300. // 生成token
  1301. String token = jwtConfig.generateToken(user.getId(), user.getRole());
  1302. LoginResultDTO result = new LoginResultDTO();
  1303. result.setToken(token);
  1304. result.setUserId(user.getId());
  1305. result.setRole(user.getRole());
  1306. result.setRoles(user.getRoles());
  1307. result.setFamilyId(user.getFamilyId());
  1308. result.setNickname(user.getNickname());
  1309. result.setIsNewUser(isNewUser);
  1310. result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
  1311. populateTeacherFields(result, user);
  1312. return result;
  1313. }
  1314. /**
  1315. * 重置用户密码(管理员功能)
  1316. * @param userId 用户ID
  1317. * @param newPassword 新密码
  1318. * @return 是否成功
  1319. */
  1320. public boolean resetPassword(Long userId, String newPassword) {
  1321. if (userId == null || newPassword == null || newPassword.length() < 6) {
  1322. return false;
  1323. }
  1324. User user = userMapper.selectById(userId);
  1325. if (user == null) {
  1326. return false;
  1327. }
  1328. user.setPassword(encryptPassword(newPassword));
  1329. user.setUpdatedAt(new Date());
  1330. return userMapper.updateById(user) > 0;
  1331. }
  1332. /**
  1333. * 设置用户的AI助手形象(吉祥物)
  1334. * @param userId 用户ID
  1335. * @param mascotCode 吉祥物代码: "xibao"/"fubao"/null
  1336. */
  1337. public void setMascot(Long userId, String mascotCode) {
  1338. User user = userMapper.selectById(userId);
  1339. if (user == null) {
  1340. throw new RuntimeException("用户不存在");
  1341. }
  1342. user.setMascot(mascotCode);
  1343. user.setUpdatedAt(new Date());
  1344. userMapper.updateById(user);
  1345. }
  1346. // ==================== Family Member API (new) ====================
  1347. /**
  1348. * 获取家庭成员列表(孩子)
  1349. */
  1350. public List<FamilyMemberInfoDTO> getFamilyMemberren(Long userId) {
  1351. User user = userMapper.selectById(userId);
  1352. if (user == null || user.getFamilyId() == null) {
  1353. return new ArrayList<>();
  1354. }
  1355. List<FamilyMember> members = familyMemberMapper.selectList(
  1356. new LambdaQueryWrapper<FamilyMember>()
  1357. .eq(FamilyMember::getFamilyId, user.getFamilyId()));
  1358. return members.stream().map(this::toFamilyMemberInfoDTO).collect(java.util.stream.Collectors.toList());
  1359. }
  1360. /**
  1361. * 创建家庭成员
  1362. */
  1363. public FamilyMemberInfoDTO createFamilyMember(Long userId, CreateFamilyMemberDTO dto) {
  1364. User user = userMapper.selectById(userId);
  1365. if (user == null) {
  1366. throw new RuntimeException("用户不存在");
  1367. }
  1368. FamilyMember member = new FamilyMember();
  1369. member.setUserId(userId);
  1370. member.setFamilyId(user.getFamilyId());
  1371. member.setNickname(dto.getNickname());
  1372. member.setPhone(dto.getPhone());
  1373. member.setAvatar(dto.getAvatar());
  1374. member.setGender(dto.getGender());
  1375. try {
  1376. if (dto.getBirthday() != null && !dto.getBirthday().isEmpty()) {
  1377. java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
  1378. member.setBirthday(sdf.parse(dto.getBirthday()));
  1379. }
  1380. } catch (Exception e) {
  1381. // ignore parse error
  1382. }
  1383. if (dto.getAge() != null) {
  1384. member.setAge(dto.getAge());
  1385. }
  1386. member.setTotalPoints(0);
  1387. member.setCreatedAt(new Date());
  1388. member.setUpdatedAt(new Date());
  1389. familyMemberMapper.insert(member);
  1390. return toFamilyMemberInfoDTO(member);
  1391. }
  1392. /**
  1393. * 更新家庭成员信息
  1394. */
  1395. public boolean updateFamilyMember(Long userId, Long memberId, UpdateFamilyMemberDTO dto) {
  1396. User user = userMapper.selectById(userId);
  1397. if (user == null || user.getFamilyId() == null) {
  1398. return false;
  1399. }
  1400. FamilyMember member = familyMemberMapper.selectById(memberId);
  1401. if (member == null || !member.getFamilyId().equals(user.getFamilyId())) {
  1402. return false;
  1403. }
  1404. if (dto.getNickname() != null) {
  1405. member.setNickname(dto.getNickname());
  1406. }
  1407. if (dto.getPhone() != null) {
  1408. member.setPhone(dto.getPhone());
  1409. }
  1410. if (dto.getAvatar() != null) {
  1411. member.setAvatar(dto.getAvatar());
  1412. }
  1413. if (dto.getGender() != null) {
  1414. member.setGender(dto.getGender());
  1415. }
  1416. try {
  1417. if (dto.getBirthday() != null && !dto.getBirthday().isEmpty()) {
  1418. java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
  1419. member.setBirthday(sdf.parse(dto.getBirthday()));
  1420. }
  1421. } catch (Exception e) {
  1422. // ignore parse error
  1423. }
  1424. if (dto.getAge() != null) {
  1425. member.setAge(dto.getAge());
  1426. }
  1427. if (dto.getShowToFamily() != null) {
  1428. member.setShowToFamily(dto.getShowToFamily());
  1429. }
  1430. member.setUpdatedAt(new Date());
  1431. return familyMemberMapper.updateById(member) > 0;
  1432. }
  1433. /**
  1434. * 家长切换到指定家庭成员身份
  1435. */
  1436. public Long switchToFamilyMember(Long userId, Long memberId) {
  1437. User user = userMapper.selectById(userId);
  1438. if (user == null) {
  1439. throw new RuntimeException("用户不存在");
  1440. }
  1441. // 验证家庭成员属于该用户所在家庭
  1442. FamilyMember member = familyMemberMapper.selectById(memberId);
  1443. if (member == null || !member.getFamilyId().equals(user.getFamilyId())) {
  1444. throw new RuntimeException("家庭成员不存在或不属于您的家庭");
  1445. }
  1446. // 切换用户role为child,记录原始角色以便切回
  1447. user.setRole("child");
  1448. user.setUpdatedAt(new Date());
  1449. userMapper.updateById(user);
  1450. return member.getId();
  1451. }
  1452. private FamilyMemberInfoDTO toFamilyMemberInfoDTO(FamilyMember member) {
  1453. FamilyMemberInfoDTO dto = new FamilyMemberInfoDTO();
  1454. dto.setId(member.getId());
  1455. dto.setNickname(member.getNickname());
  1456. dto.setAge(member.getAge());
  1457. dto.setBirthday(member.getBirthday());
  1458. dto.setGender(member.getGender());
  1459. dto.setPhone(member.getPhone());
  1460. dto.setAvatar(member.getAvatar());
  1461. dto.setGeneration(member.getGeneration());
  1462. dto.setShowToFamily(member.getShowToFamily());
  1463. dto.setTotalPoints(member.getTotalPoints());
  1464. return dto;
  1465. }
  1466. private void populateTeacherFields(LoginResultDTO result, User user) {
  1467. result.setTeacherStatus(user.getTeacherStatus() != null ? user.getTeacherStatus() : "pending");
  1468. result.setTeacherRejectReason(user.getTeacherRejectReason());
  1469. boolean hasTeacher = hasRole(user, "teacher");
  1470. result.setHasTeacherRole(hasTeacher);
  1471. }
  1472. /**
  1473. * 获取用户有效的 familyId 集合:自己的 familyId + teacherFamilyIds 中关联的家庭。
  1474. * @param userId 用户ID
  1475. * @return familyId 集合,永不为 null(空用户返回空 set)
  1476. */
  1477. public Set<Long> getEffectiveFamilyIds(Long userId) {
  1478. Set<Long> ids = new HashSet<>();
  1479. User user = userMapper.selectById(userId);
  1480. if (user == null) {
  1481. return ids;
  1482. }
  1483. if (user.getFamilyId() != null) {
  1484. ids.add(user.getFamilyId());
  1485. }
  1486. // 解析 teacherFamilyIds(逗号分隔格式)
  1487. String teacherFamilyIds = user.getTeacherFamilyIds();
  1488. if (teacherFamilyIds != null && !teacherFamilyIds.trim().isEmpty()) {
  1489. String[] parts = teacherFamilyIds.split(",");
  1490. for (String part : parts) {
  1491. String trimmed = part.trim();
  1492. if (!trimmed.isEmpty()) {
  1493. try { ids.add(Long.parseLong(trimmed)); } catch (NumberFormatException ignored) {}
  1494. }
  1495. }
  1496. }
  1497. return ids;
  1498. }
  1499. public boolean deleteUser(Long userId) {
  1500. User user = userMapper.selectById(userId);
  1501. if (user == null) {
  1502. return false;
  1503. }
  1504. return userMapper.deleteById(userId) > 0;
  1505. }
  1506. }