SysAuthServiceImpl.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. package com.backendsys.modules.system.service.impl;
  2. import cn.hutool.core.convert.Convert;
  3. import cn.hutool.core.date.DateUnit;
  4. import cn.hutool.core.date.DateUtil;
  5. import cn.hutool.core.util.RandomUtil;
  6. import cn.hutool.core.util.StrUtil;
  7. import cn.hutool.json.JSONUtil;
  8. import com.backendsys.exception.CustException;
  9. import com.backendsys.modules.common.config.redis.utils.RedisUtil;
  10. import com.backendsys.modules.common.config.security.entity.SecurityUserInfo;
  11. import com.backendsys.modules.common.config.security.utils.*;
  12. import com.backendsys.modules.system.dao.SysMobileAreaDao;
  13. import com.backendsys.modules.system.dao.SysUserDao;
  14. import com.backendsys.modules.system.dao.SysUserInfoDao;
  15. import com.backendsys.modules.system.dao.SysUserRoleDao;
  16. import com.backendsys.modules.system.entity.*;
  17. import com.backendsys.modules.system.service.SysAuthService;
  18. import com.backendsys.modules.system.service.SysCommonService;
  19. import com.backendsys.modules.system.service.SysUserIntegralService;
  20. import com.backendsys.modules.system.service.SysUserService;
  21. import com.backendsys.modules.system.utils.UserUtil;
  22. import com.backendsys.utils.response.ResultEnum;
  23. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  24. import com.google.code.kaptcha.Producer;
  25. import jakarta.servlet.ServletOutputStream;
  26. import jakarta.servlet.http.HttpServletResponse;
  27. import org.springframework.beans.factory.annotation.Autowired;
  28. import org.springframework.beans.factory.annotation.Value;
  29. import org.springframework.cache.annotation.Cacheable;
  30. import org.springframework.core.env.Environment;
  31. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  32. import org.springframework.stereotype.Service;
  33. import org.springframework.transaction.annotation.Transactional;
  34. import javax.imageio.ImageIO;
  35. import java.awt.image.BufferedImage;
  36. import java.io.ByteArrayOutputStream;
  37. import java.io.IOException;
  38. import java.time.LocalDateTime;
  39. import java.time.ZoneOffset;
  40. import java.util.*;
  41. import java.util.concurrent.TimeUnit;
  42. @Service
  43. public class SysAuthServiceImpl implements SysAuthService {
  44. @Autowired
  45. private Environment env;
  46. @Autowired
  47. private UserUtil userUtil;
  48. @Autowired
  49. private JwtUtil jwtUtil;
  50. @Autowired
  51. private RedisUtil redisUtil;
  52. @Autowired
  53. private TokenUtil tokenUtil;
  54. @Autowired
  55. private HttpRequestUtil httpRequestUtil;
  56. @Autowired
  57. private LockStatusUtil lockStatusUtil;
  58. @Autowired
  59. private CaptchaUtil captchaUtil;
  60. @Autowired
  61. private Producer captchaProducer;
  62. @Autowired
  63. private SysUserDao sysUserDao;
  64. @Autowired
  65. private SysUserRoleDao sysUserRoleDao;
  66. @Autowired
  67. private SysUserInfoDao sysUserInfoDao;
  68. @Autowired
  69. private SysUserService sysUserService;
  70. @Autowired
  71. private SysMobileAreaDao sysMobileAreaDao;
  72. @Autowired
  73. private SysUserIntegralService sysUserIntegralService;
  74. @Autowired
  75. private SysCommonService sysCommonService;
  76. @Value("${tencent.sms.debug}")
  77. private String SMS_DEBUG;
  78. @Value("${CAPTCHA_DURATION}")
  79. private Integer CAPTCHA_DURATION;
  80. @Value("${REDIS_LOGIN_TOKEN_PREFIX}")
  81. private String REDIS_LOGIN_TOKEN_PREFIX;
  82. @Value("${spring.application.name}")
  83. private String APPLICATION_NAME;
  84. @Override
  85. public void renderCaptcha(HttpServletResponse response) throws IOException {
  86. byte[] captchaChallengeAsJpeg;
  87. ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
  88. try {
  89. String createText = captchaProducer.createText();
  90. // 获得当前 (UA + IP) 生成的 Key
  91. String captchaRedisKey = httpRequestUtil.getKaptchaKey();
  92. // 保存 验证码字符串 到 redis 中
  93. redisUtil.setCacheObject(captchaRedisKey, createText, this.CAPTCHA_DURATION, TimeUnit.MILLISECONDS);
  94. // 返回 BufferedImage 对象并转为 byte 写入到 byte 数组中
  95. BufferedImage challenge = captchaProducer.createImage(createText);
  96. ImageIO.write(challenge, "jpg", jpegOutputStream);
  97. } catch (Exception e) {
  98. response.sendError(HttpServletResponse.SC_NOT_FOUND);
  99. }
  100. // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
  101. captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
  102. response.setHeader("Cache-Control", "no-store");
  103. response.setHeader("Pragma", "no-cache");
  104. response.setDateHeader("Expires", 0);
  105. response.setContentType("image/jpeg");
  106. ServletOutputStream responseOutputStream = response.getOutputStream();
  107. responseOutputStream.write(captchaChallengeAsJpeg);
  108. responseOutputStream.flush();
  109. responseOutputStream.close();
  110. }
  111. // 判断是否需验证码登录状态
  112. @Override
  113. public Map<String, Object> checkCaptchaRequired(String username) {
  114. if (StrUtil.isEmpty(username)) throw new CustException("username 不能为空");
  115. Boolean currentCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
  116. return Map.of("is_captcha_required", currentCaptchaRequired);
  117. }
  118. @Override
  119. @Cacheable(value = "catch::mobile-area", key = "'list'", unless = "#result == null")
  120. public List<SysMobileArea> getMobileAreaList(SysMobileArea sysMobileArea) {
  121. return sysMobileAreaDao.selectMobileAreaList(sysMobileArea);
  122. }
  123. // [方法] 登录失败 (通用) (errMsg: 错误提示文本, username: 用户名, intercept: 是否拦截)
  124. public void loginFail(String errMsg, String username, Boolean isIntercept) {
  125. // 验证码是否必填
  126. Boolean currentCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
  127. System.out.println("(loginFailByUsername) currentCaptchaRequired = " + currentCaptchaRequired);
  128. // 删除图形验证码
  129. redisUtil.delete(httpRequestUtil.getKaptchaKey());
  130. // 添加登录错误的冻结标记
  131. if (isIntercept) lockStatusUtil.setLockStatus(APPLICATION_NAME + "-login-error", username);
  132. if (currentCaptchaRequired) {
  133. throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode(), Map.of("is_captcha_required", true));
  134. } else {
  135. throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode());
  136. }
  137. }
  138. // [方法] 登录成功
  139. public SysUserInfo loginSuccess(Long user_id, Integer is_remember) {
  140. // [查询] 登录的用户信息
  141. SysUserInfo entity = new SysUserInfo();
  142. entity.setUser_id(user_id);
  143. SysUserInfo sysUserInfo = sysUserService.selectUserInfo(entity);
  144. // 删除图形验证码缓存
  145. redisUtil.delete(httpRequestUtil.getKaptchaKey());
  146. // 删除旧的登录缓存
  147. tokenUtil.deleteRedisLoginToken(sysUserInfo.getLast_login_uuid());
  148. // 判断用户是否审核
  149. Integer audit_status = sysUserInfo.getAudit_status();
  150. if (audit_status != null && audit_status.equals(1)) throw new CustException("用户正在审核中");
  151. if (audit_status != null && audit_status.equals(-1)) throw new CustException("用户审核未通过,请与客服联系");
  152. // 判断用户是否启用
  153. Integer status = sysUserInfo.getStatus();
  154. if (status != null && status.equals(-1)) throw new CustException("该用户已被禁用,请与客服联系");
  155. // 判断用户是否已删除
  156. Integer del_flag = sysUserInfo.getDel_flag();
  157. if (del_flag != null && del_flag.equals(1)) throw new CustException("当前用户不可用,请与客服联系");
  158. // 设置 最后一次的登录信息 (uuid, ip, 登录时间)
  159. String uuid = Convert.toStr(UUID.randomUUID());
  160. sysUserInfo.setLast_login_uuid(uuid);
  161. sysUserInfo.setLast_login_ip(httpRequestUtil.getIpAddr());
  162. // sysUserInfo.setLast_login_time(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
  163. sysUserInfo.setLast_login_time(LocalDateTime.now(ZoneOffset.UTC));
  164. sysUserInfoDao.updateById(sysUserInfo);
  165. // [系统配置] 系统用户默认登录过期时间(小时)
  166. Integer SYSTEM_USER_LOGIN_DURATION_DEFAULT = Convert.toInt(sysCommonService.getCommonByTag("SYSTEM_USER_LOGIN_DURATION_DEFAULT"));
  167. // 将小时转换为毫秒
  168. Long DEFAULT_MILLISECONDS = SYSTEM_USER_LOGIN_DURATION_DEFAULT * DateUnit.HOUR.getMillis();
  169. // 7天 (转毫秒)
  170. Long SEVEN_DAY_MILLISECONDS = 7L * 24 * 60 * 60 * 1000;
  171. // 是否钩选 "7天免登录",否则按系统配置-默认过期时间
  172. Long token_duration_milliseconds = (is_remember != null && is_remember.equals(1)) ? SEVEN_DAY_MILLISECONDS : DEFAULT_MILLISECONDS;
  173. Integer tokenDurationHours = Convert.toInt(token_duration_milliseconds / 3600000L);
  174. Date token_expiration = new Date((new Date()).getTime() + token_duration_milliseconds);
  175. sysUserInfo.setToken_expiration(DateUtil.format(token_expiration, "yyyy-MM-dd HH:mm:ss"));
  176. // 实时更新缓存: 用户信息
  177. sysUserInfo = userUtil.syncUpdateUserCatch(sysUserInfo, tokenDurationHours);
  178. //// 生成 Token
  179. //SecurityUserInfo securityUserInfo = JSONUtil.toBean(JSONUtil.parseObj(sysUserInfo), SecurityUserInfo.class);
  180. //String token = jwtUtil.createSystemJwtToken(securityUserInfo);
  181. //sysUserInfo.setToken(token);
  182. //
  183. //// 生成 PerMissionIds
  184. //List<String> permission_ids_list = sysUserInfo.getPermission_ids();
  185. //
  186. //// [Redis] 将 Token 与 Permission 存入缓存
  187. //String token_redis_key = REDIS_LOGIN_TOKEN_PREFIX + uuid;
  188. //TokenCatch tokenCatch = new TokenCatch(token, permission_ids_list);
  189. //redisUtil.setCacheObject(token_redis_key, JSONUtil.toJsonStr(tokenCatch), token_duration_hours, TimeUnit.HOURS);
  190. return sysUserInfo;
  191. }
  192. private void setLoginRequired(String key) {
  193. Object captchaValue = redisUtil.getCacheObject(APPLICATION_NAME + "-login-required-captcha-" + key);
  194. Integer currentErrCount = (captchaValue == null) ? 1 : (Convert.toInt(captchaValue) + 1);
  195. redisUtil.setCacheObject(APPLICATION_NAME + "-login-required-captcha-" + key, currentErrCount, 1, TimeUnit.MINUTES);
  196. System.out.println("currentErrCount: " + currentErrCount);
  197. }
  198. public void cleanLoginRequired(String key) {
  199. redisUtil.delete(APPLICATION_NAME + "-login-required-captcha-" + key);
  200. }
  201. /**
  202. * 登录 (用户名)
  203. */
  204. @Override
  205. @Transactional(rollbackFor = Exception.class)
  206. public SysUserInfo login(SysAuth sysAuth) {
  207. String username = sysAuth.getUsername();
  208. String password = sysAuth.getPassword();
  209. String captcha = sysAuth.getCaptcha();
  210. // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示)
  211. lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-login-error", username);
  212. // -- 判断是否需要输入验证码 ----------------------------------------------------
  213. // - 当输错 3 次密码时,需要输入验证码
  214. // - 当输错后 1 分钟后重置
  215. Boolean isCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
  216. if (isCaptchaRequired) {
  217. Boolean isCaptchaEmpty = StrUtil.isEmpty(captcha);
  218. Boolean isCpatchaValid = (captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey()));
  219. if (isCaptchaEmpty) { loginFail("验证码不能为空", username, false); return null; }
  220. if (!isCpatchaValid) { loginFail("验证码错误", username, false); return null; }
  221. }
  222. // --------------------------------------------------------------------------
  223. // [Method] 判断 用户 是否存在 && 密码是否正确
  224. SysUser sysUser = sysUserDao.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
  225. if (sysUser == null) {
  226. // 输入错误时,计数器叠加,并且设置重置时间 (会一直叠加,直到重置 或 登录成功)
  227. setLoginRequired(username);
  228. // [登录失败] 用户不存在
  229. loginFail("用户名或密码错误", username, true);
  230. return null;
  231. } else {
  232. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  233. if (!encoder.matches(password, sysUser.getPassword())) {
  234. // 输入错误时,计数器叠加,并且设置重置时间 (会一直叠加,直到重置 或 登录成功)
  235. setLoginRequired(username);
  236. // [登录失败] 密码不正确
  237. loginFail("用户名或密码错误", username, true);
  238. }
  239. // [登录成功]
  240. cleanLoginRequired(username);
  241. return loginSuccess(sysUser.getId(), sysAuth.getIs_remember());
  242. }
  243. }
  244. /**
  245. * 登录 (手机号码)
  246. */
  247. @Override
  248. @Transactional(rollbackFor = Exception.class)
  249. public SysUserInfo loginWithPhone(SysAuthPhone sysAuthPhone) {
  250. String phone = sysAuthPhone.getPhone();
  251. Integer phoneAreaCode = sysAuthPhone.getPhone_area_code();
  252. Integer phoneValidCode = sysAuthPhone.getPhone_valid_code();
  253. // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示)
  254. lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-login-error", phone);
  255. // 判断短信验证码是否正确
  256. String redisKey = APPLICATION_NAME + "-sms-login" + "-" + phone;
  257. Integer smsCode = redisUtil.getCacheObject(redisKey);
  258. // 判断是否发送验证码
  259. if ("false".equals(SMS_DEBUG) && smsCode == null) throw new CustException("请先发送短信验证码");
  260. // 判断短信验证码是否错误
  261. if ("false".equals(SMS_DEBUG) && !smsCode.equals(phoneValidCode)) loginFail("短信验证码错误", phone, true);
  262. // 判断手机号是否存在
  263. LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
  264. queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
  265. SysUser sysUser = sysUserDao.selectOne(queryWrapper);
  266. if (sysUser == null) {
  267. // [登录失败] 用户不存在 (并不会销毁短信验证码)
  268. loginFail("手机号码未注册,请先注册", phone, true);
  269. return null;
  270. } else {
  271. // 登录成功,销毁短信验证码
  272. redisUtil.delete(redisKey);
  273. // [登录成功]
  274. return loginSuccess(sysUser.getId(), sysAuthPhone.getIs_remember());
  275. }
  276. }
  277. @Override
  278. @Transactional(rollbackFor = Exception.class)
  279. public Map<String, Object> register(SysUserDTO sysUserDTO) {
  280. // 判断是否允许注册
  281. // [系统配置] 是否允许系统用户注册
  282. Boolean SYSTEM_USER_ALLOW_REGISTER = Convert.toBool(sysCommonService.getCommonByTag("SYSTEM_USER_ALLOW_REGISTER"));
  283. if (!SYSTEM_USER_ALLOW_REGISTER) throw new CustException("系统已禁止注册");
  284. // -- 参数校验 --------------------------------------------------------------
  285. String username = sysUserDTO.getUsername();
  286. String password = sysUserDTO.getPassword();
  287. String captcha = sysUserDTO.getCaptcha();
  288. String phone = sysUserDTO.getPhone();
  289. Integer phoneAreaCode = sysUserDTO.getPhone_area_code();
  290. Integer phoneValidCode = sysUserDTO.getPhone_valid_code();
  291. String activeProfile = env.getActiveProfiles()[0];
  292. if (!"local".equals(activeProfile)) {
  293. // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示)
  294. lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-register-error", username);
  295. lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-register-error", phone);
  296. // 判断图形验证码是否正确
  297. if (!captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey())) {
  298. loginFail("验证码错误", username, false);
  299. return null;
  300. }
  301. }
  302. // [查询] 判断用户名是否存在
  303. SysUser sysUser1 = sysUserDao.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
  304. if (sysUser1 != null) throw new CustException("用户名 (" + username + ") 已被注册");
  305. // 判断短信验证码是否正确
  306. if (!"local".equals(activeProfile)) {
  307. String redisKey = APPLICATION_NAME + "-sms-register" + "-" + phone;
  308. Integer smsCode = redisUtil.getCacheObject(redisKey);
  309. // 判断是否发送验证码
  310. if ("false".equals(SMS_DEBUG) && smsCode == null) throw new CustException("请先发送短信验证码");
  311. // 判断短信验证码是否错误
  312. if ("false".equals(SMS_DEBUG) && !smsCode.equals(phoneValidCode)) loginFail("短信验证码错误", phone, true);
  313. }
  314. // [查询] 判断手机号是否存在
  315. LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
  316. queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
  317. SysUser sysUser2 = sysUserDao.selectOne(queryWrapper);
  318. if (sysUser2 != null) throw new CustException("手机号码 (+" + phoneAreaCode + " " + phone + ") 已被注册");
  319. // -- 通过校验 --------------------------------------------------------------
  320. // 密码二次加密
  321. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  322. String encodedPassword = encoder.encode(password);
  323. sysUserDTO.setPassword(encodedPassword);
  324. // 注册
  325. SysUserDTO registerEntity = new SysUserDTO();
  326. registerEntity.setUsername(sysUserDTO.getUsername());
  327. registerEntity.setPhone(sysUserDTO.getPhone());
  328. registerEntity.setPhone_area_code(sysUserDTO.getPhone_area_code());
  329. registerEntity.setPassword(sysUserDTO.getPassword());
  330. // 做成后台可控制?
  331. // 邀请码
  332. registerEntity.setInvite_code(sysUserDTO.getInvite_code());
  333. // 注册时,默认使用 权限 (DEFAULT)
  334. String role_sign = "DEFAULT";
  335. // 如果邀请码是 (Material),则注册成为 [素材游客]
  336. if ("Material".equals(sysUserDTO.getInvite_code())) role_sign = "MATERIAL_GUEST";
  337. LambdaQueryWrapper<SysUserRole> wrapperRole = new LambdaQueryWrapper<>();
  338. wrapperRole.eq(SysUserRole::getRole_sign, role_sign);
  339. SysUserRole roleDetail = sysUserRoleDao.selectOne(wrapperRole);
  340. registerEntity.setRole_id(Arrays.asList(roleDetail.getRole_id()));
  341. // 注册时,审核状态为 待审核 (-1拒绝, 1待审核, 2审核通过)
  342. registerEntity.setAudit_status(1);
  343. // 注册时,状态为 禁用
  344. // registerEntity.setStatus(-1);
  345. // 随机昵称 (6位)
  346. registerEntity.setNickname("用户" + RandomUtil.randomStringUpper(6));
  347. // 创建用户
  348. sysUserDao.insertUser(registerEntity);
  349. // 初始化用户积分
  350. sysUserIntegralService.init(registerEntity.getId());
  351. return Map.of("user_id", registerEntity.getId());
  352. }
  353. /**
  354. * 忘记密码/重置密码
  355. */
  356. @Override
  357. public Map<String, Object> forgotPassword(SysUserDTO sysUserDTO) {
  358. String phone = sysUserDTO.getPhone();
  359. Integer phoneAreaCode = sysUserDTO.getPhone_area_code();
  360. Integer phoneValidCode = sysUserDTO.getPhone_valid_code();
  361. // 判断短信验证码是否正确
  362. String redisKey = APPLICATION_NAME + "-sms-forgotPassword-" + sysUserDTO.getPhone();
  363. Integer smsCode = redisUtil.getCacheObject(redisKey);
  364. if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) {
  365. throw new CustException("短信验证码错误");
  366. }
  367. // [查询] 判断手机号是否存在
  368. LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
  369. queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
  370. SysUser sysUser = sysUserDao.selectOne(queryWrapper);
  371. if (sysUser == null) throw new CustException("手机号码不存在");
  372. // 密码二次加密
  373. String password = sysUserDTO.getPassword();
  374. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  375. String encodedPassword = encoder.encode(password);
  376. sysUser.setPassword(encodedPassword);
  377. // 编辑密码
  378. sysUserDao.updateById(sysUser);
  379. // 更改成功,销毁短信验证码
  380. redisUtil.delete(redisKey);
  381. return Map.of("user_id", sysUser.getId());
  382. }
  383. /**
  384. * 退出登录
  385. */
  386. public Map<String, Object> logout() {
  387. Long user_id = httpRequestUtil.getUserId();
  388. if (user_id != null) {
  389. tokenUtil.deleteRedisLoginToken(null);
  390. }
  391. return Map.of("user_id", user_id);
  392. }
  393. }