提交代码

dev-discount
胡锐 3 years ago
parent ee59acc4e8
commit bfb05873ca
  1. 99
      hai-cweb/src/main/java/com/cweb/controller/WechatController.java
  2. 13
      hai-service/src/main/java/com/hai/dao/HighUserMapper.java
  3. 14
      hai-service/src/main/java/com/hai/dao/HighUserSqlProvider.java
  4. 19
      hai-service/src/main/java/com/hai/entity/HighUser.java
  5. 60
      hai-service/src/main/java/com/hai/entity/HighUserExample.java
  6. 8
      hai-service/src/main/java/com/hai/service/impl/HighUserServiceImpl.java

@ -17,6 +17,7 @@ import com.hai.common.exception.SysCode;
import com.hai.common.security.SessionObject; import com.hai.common.security.SessionObject;
import com.hai.common.security.UserCenter; import com.hai.common.security.UserCenter;
import com.hai.common.utils.HttpsUtils; import com.hai.common.utils.HttpsUtils;
import com.hai.common.utils.IDGenerator;
import com.hai.common.utils.RedisUtil; import com.hai.common.utils.RedisUtil;
import com.hai.common.utils.ResponseMsgUtil; import com.hai.common.utils.ResponseMsgUtil;
import com.hai.entity.HighUser; import com.hai.entity.HighUser;
@ -116,6 +117,80 @@ public class WechatController {
} }
} }
@RequestMapping(value = "/loginByPhone", method = RequestMethod.GET)
@ResponseBody
@ApiOperation(value = "小程序根据手机号登录或注册")
public ResponseData loginByPhone(@RequestParam(value = "encryptedData", required = true) String encryptedData,
@RequestParam(value = "iv", required = true) String iv,
@RequestParam(value = "openId", required = true) String openId,
HttpServletRequest request, HttpServletResponse response) {
try {
log.error("origin encryptedData:" + encryptedData + ";iv:" + iv);
//校验openId不能为空
//encryptedData,iv与unionId不能同时为空
encryptedData = encryptedData.replace(" ", "+");
iv = iv.replace(" ", "+");
log.error("dest encryptedData:" + encryptedData + ";iv:" + iv);
if (StringUtils.isEmpty(openId)
|| StringUtils.isEmpty(encryptedData) || StringUtils.isEmpty(iv)) {
throw ErrorHelp.genException(SysCode.MiniProgram, ErrorCode.REQ_PARAMS_ERROR, "");
}
//请求微信api,获取用户session_key以及openId
Object skObject = redisUtil.hget(WX_OPENID_SESSION_REDIS,openId);
if (skObject == null){
throw ErrorHelp.genException(SysCode.MiniProgram, ErrorCode.WECHAT_LOGIN_ERROR);
}
WxMaJscode2SessionResult session = (WxMaJscode2SessionResult)skObject;
final WxMaService wxService = WxMaConfiguration.getMaService();
WxMaPhoneNumberInfo phoneNoInfo = wxService.getUserService().getPhoneNoInfo(session.getSessionKey(), encryptedData, iv);
if (phoneNoInfo == null || StringUtils.isEmpty(phoneNoInfo.getPurePhoneNumber())) {
throw ErrorHelp.genException(SysCode.MiniProgram, ErrorCode.WECHAT_DECRYPT_ERROR, "用户phoneNumber解析失败");
}
HighUser user = highUserService.findByOpenId(session.getOpenid());
if (user != null) {
if (StringUtils.isBlank(user.getPhone())) {
if (highUserService.findByPhone(phoneNoInfo.getPurePhoneNumber()) != null) {
throw ErrorHelp.genException(SysCode.MiniProgram, ErrorCode.COMMON_ERROR, "手机号已被绑定,请更换后重新授权");
}
user.setPhone(phoneNoInfo.getPurePhoneNumber());
}
} else {
if (highUserService.findByPhone(phoneNoInfo.getPurePhoneNumber()) != null) {
throw ErrorHelp.genException(SysCode.MiniProgram, ErrorCode.COMMON_ERROR, "手机号已被绑定,请更换后重新授权");
}
user = new HighUser();
user.setName("用户" + IDGenerator.nextId(5));
user.setPhone(phoneNoInfo.getPurePhoneNumber());
user.setRegTime(new Date());
user.setGold(0);
user.setOpenId(session.getOpenid());
user.setUnionId(session.getUnionid());
user.setStatus(1);
// 用户信息完整状态
// 0:完整
// 1:需要填写用户信息
// 2:需要填写手机号
user.setInfoCompleteStatus(1);
highUserService.insertUser(user);
}
// 定义个人所有数据
HighUserModel highUserModel = new HighUserModel();
HighUser detailData = highUserService.getDetailDataByUser(user.getId());
detailData.setPassword(null);
highUserModel.setHighUser(detailData);
SessionObject so = new SessionObject(user.getUnionId(), 1 , highUserModel);
userCenter.save(request, response, so);
return ResponseMsgUtil.success(so);
} catch (Exception e) {
return ResponseMsgUtil.exception(e);
}
}
@RequestMapping(value = "/getPhoneNumber", method = RequestMethod.GET) @RequestMapping(value = "/getPhoneNumber", method = RequestMethod.GET)
@ResponseBody @ResponseBody
@ApiOperation(value = "小程序获取用户手机号码") @ApiOperation(value = "小程序获取用户手机号码")
@ -206,8 +281,20 @@ public class WechatController {
user.setGold(0); user.setGold(0);
user.setHeaderImg(userInfo.getAvatarUrl()); user.setHeaderImg(userInfo.getAvatarUrl());
user.setName(userInfo.getNickName()); user.setName(userInfo.getNickName());
user.setInfoCompleteStatus(2);
highUserService.insertUser(user); highUserService.insertUser(user);
} else { } else {
// 用户信息完整状态
// 0:完整
// 1:需要填写用户信息
// 2:需要填写手机号
if (user.getInfoCompleteStatus() == 1) {
if (StringUtils.isNotBlank(user.getPhone())) {
user.setInfoCompleteStatus(0);
} else {
user.setInfoCompleteStatus(2);
}
}
user.setUnionId(session.getUnionid()); user.setUnionId(session.getUnionid());
user.setHeaderImg(userInfo.getAvatarUrl()); user.setHeaderImg(userInfo.getAvatarUrl());
user.setName(userInfo.getNickName()); user.setName(userInfo.getNickName());
@ -323,8 +410,20 @@ public class WechatController {
user.setGold(0); user.setGold(0);
user.setHeaderImg(userInfo.getString("headimgurl")); user.setHeaderImg(userInfo.getString("headimgurl"));
user.setName(userInfo.getString("nickname")); user.setName(userInfo.getString("nickname"));
user.setInfoCompleteStatus(2);
highUserService.insertUser(user); highUserService.insertUser(user);
} else { } else {
// 用户信息完整状态
// 0:完整
// 1:需要填写用户信息
// 2:需要填写手机号
if (user.getInfoCompleteStatus() == 1) {
if (StringUtils.isNotBlank(user.getPhone())) {
user.setInfoCompleteStatus(0);
} else {
user.setInfoCompleteStatus(2);
}
}
user.setOpenIdH5(openid); user.setOpenIdH5(openid);
user.setHeaderImg(userInfo.getString("headimgurl")); user.setHeaderImg(userInfo.getString("headimgurl"));
user.setName(userInfo.getString("nickname")); user.setName(userInfo.getString("nickname"));

@ -44,15 +44,15 @@ public interface HighUserMapper extends HighUserMapperExt {
"sex, birthdate, reg_time, ", "sex, birthdate, reg_time, ",
"gold, `status`, open_id, ", "gold, `status`, open_id, ",
"open_id_h5, union_id, ", "open_id_h5, union_id, ",
"ext_1, ext_2, ext_3, ", "info_complete_status, ext_1, ",
"ext_4)", "ext_2, ext_3, ext_4)",
"values (#{headerImg,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, ", "values (#{headerImg,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, ",
"#{phone,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, ", "#{phone,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, ",
"#{sex,jdbcType=VARCHAR}, #{birthdate,jdbcType=DATE}, #{regTime,jdbcType=TIMESTAMP}, ", "#{sex,jdbcType=VARCHAR}, #{birthdate,jdbcType=DATE}, #{regTime,jdbcType=TIMESTAMP}, ",
"#{gold,jdbcType=INTEGER}, #{status,jdbcType=INTEGER}, #{openId,jdbcType=VARCHAR}, ", "#{gold,jdbcType=INTEGER}, #{status,jdbcType=INTEGER}, #{openId,jdbcType=VARCHAR}, ",
"#{openIdH5,jdbcType=VARCHAR}, #{unionId,jdbcType=VARCHAR}, ", "#{openIdH5,jdbcType=VARCHAR}, #{unionId,jdbcType=VARCHAR}, ",
"#{ext1,jdbcType=VARCHAR}, #{ext2,jdbcType=VARCHAR}, #{ext3,jdbcType=VARCHAR}, ", "#{infoCompleteStatus,jdbcType=INTEGER}, #{ext1,jdbcType=VARCHAR}, ",
"#{ext4,jdbcType=VARCHAR})" "#{ext2,jdbcType=VARCHAR}, #{ext3,jdbcType=VARCHAR}, #{ext4,jdbcType=VARCHAR})"
}) })
@Options(useGeneratedKeys=true,keyProperty="id") @Options(useGeneratedKeys=true,keyProperty="id")
int insert(HighUser record); int insert(HighUser record);
@ -76,6 +76,7 @@ public interface HighUserMapper extends HighUserMapperExt {
@Result(column="open_id", property="openId", jdbcType=JdbcType.VARCHAR), @Result(column="open_id", property="openId", jdbcType=JdbcType.VARCHAR),
@Result(column="open_id_h5", property="openIdH5", jdbcType=JdbcType.VARCHAR), @Result(column="open_id_h5", property="openIdH5", jdbcType=JdbcType.VARCHAR),
@Result(column="union_id", property="unionId", jdbcType=JdbcType.VARCHAR), @Result(column="union_id", property="unionId", jdbcType=JdbcType.VARCHAR),
@Result(column="info_complete_status", property="infoCompleteStatus", jdbcType=JdbcType.INTEGER),
@Result(column="ext_1", property="ext1", jdbcType=JdbcType.VARCHAR), @Result(column="ext_1", property="ext1", jdbcType=JdbcType.VARCHAR),
@Result(column="ext_2", property="ext2", jdbcType=JdbcType.VARCHAR), @Result(column="ext_2", property="ext2", jdbcType=JdbcType.VARCHAR),
@Result(column="ext_3", property="ext3", jdbcType=JdbcType.VARCHAR), @Result(column="ext_3", property="ext3", jdbcType=JdbcType.VARCHAR),
@ -86,7 +87,7 @@ public interface HighUserMapper extends HighUserMapperExt {
@Select({ @Select({
"select", "select",
"id, header_img, `name`, phone, `password`, sex, birthdate, reg_time, gold, `status`, ", "id, header_img, `name`, phone, `password`, sex, birthdate, reg_time, gold, `status`, ",
"open_id, open_id_h5, union_id, ext_1, ext_2, ext_3, ext_4", "open_id, open_id_h5, union_id, info_complete_status, ext_1, ext_2, ext_3, ext_4",
"from high_user", "from high_user",
"where id = #{id,jdbcType=BIGINT}" "where id = #{id,jdbcType=BIGINT}"
}) })
@ -104,6 +105,7 @@ public interface HighUserMapper extends HighUserMapperExt {
@Result(column="open_id", property="openId", jdbcType=JdbcType.VARCHAR), @Result(column="open_id", property="openId", jdbcType=JdbcType.VARCHAR),
@Result(column="open_id_h5", property="openIdH5", jdbcType=JdbcType.VARCHAR), @Result(column="open_id_h5", property="openIdH5", jdbcType=JdbcType.VARCHAR),
@Result(column="union_id", property="unionId", jdbcType=JdbcType.VARCHAR), @Result(column="union_id", property="unionId", jdbcType=JdbcType.VARCHAR),
@Result(column="info_complete_status", property="infoCompleteStatus", jdbcType=JdbcType.INTEGER),
@Result(column="ext_1", property="ext1", jdbcType=JdbcType.VARCHAR), @Result(column="ext_1", property="ext1", jdbcType=JdbcType.VARCHAR),
@Result(column="ext_2", property="ext2", jdbcType=JdbcType.VARCHAR), @Result(column="ext_2", property="ext2", jdbcType=JdbcType.VARCHAR),
@Result(column="ext_3", property="ext3", jdbcType=JdbcType.VARCHAR), @Result(column="ext_3", property="ext3", jdbcType=JdbcType.VARCHAR),
@ -134,6 +136,7 @@ public interface HighUserMapper extends HighUserMapperExt {
"open_id = #{openId,jdbcType=VARCHAR},", "open_id = #{openId,jdbcType=VARCHAR},",
"open_id_h5 = #{openIdH5,jdbcType=VARCHAR},", "open_id_h5 = #{openIdH5,jdbcType=VARCHAR},",
"union_id = #{unionId,jdbcType=VARCHAR},", "union_id = #{unionId,jdbcType=VARCHAR},",
"info_complete_status = #{infoCompleteStatus,jdbcType=INTEGER},",
"ext_1 = #{ext1,jdbcType=VARCHAR},", "ext_1 = #{ext1,jdbcType=VARCHAR},",
"ext_2 = #{ext2,jdbcType=VARCHAR},", "ext_2 = #{ext2,jdbcType=VARCHAR},",
"ext_3 = #{ext3,jdbcType=VARCHAR},", "ext_3 = #{ext3,jdbcType=VARCHAR},",

@ -76,6 +76,10 @@ public class HighUserSqlProvider {
sql.VALUES("union_id", "#{unionId,jdbcType=VARCHAR}"); sql.VALUES("union_id", "#{unionId,jdbcType=VARCHAR}");
} }
if (record.getInfoCompleteStatus() != null) {
sql.VALUES("info_complete_status", "#{infoCompleteStatus,jdbcType=INTEGER}");
}
if (record.getExt1() != null) { if (record.getExt1() != null) {
sql.VALUES("ext_1", "#{ext1,jdbcType=VARCHAR}"); sql.VALUES("ext_1", "#{ext1,jdbcType=VARCHAR}");
} }
@ -114,6 +118,7 @@ public class HighUserSqlProvider {
sql.SELECT("open_id"); sql.SELECT("open_id");
sql.SELECT("open_id_h5"); sql.SELECT("open_id_h5");
sql.SELECT("union_id"); sql.SELECT("union_id");
sql.SELECT("info_complete_status");
sql.SELECT("ext_1"); sql.SELECT("ext_1");
sql.SELECT("ext_2"); sql.SELECT("ext_2");
sql.SELECT("ext_3"); sql.SELECT("ext_3");
@ -187,6 +192,10 @@ public class HighUserSqlProvider {
sql.SET("union_id = #{record.unionId,jdbcType=VARCHAR}"); sql.SET("union_id = #{record.unionId,jdbcType=VARCHAR}");
} }
if (record.getInfoCompleteStatus() != null) {
sql.SET("info_complete_status = #{record.infoCompleteStatus,jdbcType=INTEGER}");
}
if (record.getExt1() != null) { if (record.getExt1() != null) {
sql.SET("ext_1 = #{record.ext1,jdbcType=VARCHAR}"); sql.SET("ext_1 = #{record.ext1,jdbcType=VARCHAR}");
} }
@ -224,6 +233,7 @@ public class HighUserSqlProvider {
sql.SET("open_id = #{record.openId,jdbcType=VARCHAR}"); sql.SET("open_id = #{record.openId,jdbcType=VARCHAR}");
sql.SET("open_id_h5 = #{record.openIdH5,jdbcType=VARCHAR}"); sql.SET("open_id_h5 = #{record.openIdH5,jdbcType=VARCHAR}");
sql.SET("union_id = #{record.unionId,jdbcType=VARCHAR}"); sql.SET("union_id = #{record.unionId,jdbcType=VARCHAR}");
sql.SET("info_complete_status = #{record.infoCompleteStatus,jdbcType=INTEGER}");
sql.SET("ext_1 = #{record.ext1,jdbcType=VARCHAR}"); sql.SET("ext_1 = #{record.ext1,jdbcType=VARCHAR}");
sql.SET("ext_2 = #{record.ext2,jdbcType=VARCHAR}"); sql.SET("ext_2 = #{record.ext2,jdbcType=VARCHAR}");
sql.SET("ext_3 = #{record.ext3,jdbcType=VARCHAR}"); sql.SET("ext_3 = #{record.ext3,jdbcType=VARCHAR}");
@ -286,6 +296,10 @@ public class HighUserSqlProvider {
sql.SET("union_id = #{unionId,jdbcType=VARCHAR}"); sql.SET("union_id = #{unionId,jdbcType=VARCHAR}");
} }
if (record.getInfoCompleteStatus() != null) {
sql.SET("info_complete_status = #{infoCompleteStatus,jdbcType=INTEGER}");
}
if (record.getExt1() != null) { if (record.getExt1() != null) {
sql.SET("ext_1 = #{ext1,jdbcType=VARCHAR}"); sql.SET("ext_1 = #{ext1,jdbcType=VARCHAR}");
} }

@ -67,9 +67,6 @@ public class HighUser extends UserModel implements Serializable {
private String openId; private String openId;
/**
* H5openId
*/
private String openIdH5; private String openIdH5;
/** /**
@ -77,6 +74,11 @@ public class HighUser extends UserModel implements Serializable {
*/ */
private String unionId; private String unionId;
/**
* 用户信息完整状态 0完整 1需要填写用户信息 2需要填写手机号
*/
private Integer infoCompleteStatus;
private String ext1; private String ext1;
private String ext2; private String ext2;
@ -191,6 +193,14 @@ public class HighUser extends UserModel implements Serializable {
this.unionId = unionId; this.unionId = unionId;
} }
public Integer getInfoCompleteStatus() {
return infoCompleteStatus;
}
public void setInfoCompleteStatus(Integer infoCompleteStatus) {
this.infoCompleteStatus = infoCompleteStatus;
}
public String getExt1() { public String getExt1() {
return ext1; return ext1;
} }
@ -248,6 +258,7 @@ public class HighUser extends UserModel implements Serializable {
&& (this.getOpenId() == null ? other.getOpenId() == null : this.getOpenId().equals(other.getOpenId())) && (this.getOpenId() == null ? other.getOpenId() == null : this.getOpenId().equals(other.getOpenId()))
&& (this.getOpenIdH5() == null ? other.getOpenIdH5() == null : this.getOpenIdH5().equals(other.getOpenIdH5())) && (this.getOpenIdH5() == null ? other.getOpenIdH5() == null : this.getOpenIdH5().equals(other.getOpenIdH5()))
&& (this.getUnionId() == null ? other.getUnionId() == null : this.getUnionId().equals(other.getUnionId())) && (this.getUnionId() == null ? other.getUnionId() == null : this.getUnionId().equals(other.getUnionId()))
&& (this.getInfoCompleteStatus() == null ? other.getInfoCompleteStatus() == null : this.getInfoCompleteStatus().equals(other.getInfoCompleteStatus()))
&& (this.getExt1() == null ? other.getExt1() == null : this.getExt1().equals(other.getExt1())) && (this.getExt1() == null ? other.getExt1() == null : this.getExt1().equals(other.getExt1()))
&& (this.getExt2() == null ? other.getExt2() == null : this.getExt2().equals(other.getExt2())) && (this.getExt2() == null ? other.getExt2() == null : this.getExt2().equals(other.getExt2()))
&& (this.getExt3() == null ? other.getExt3() == null : this.getExt3().equals(other.getExt3())) && (this.getExt3() == null ? other.getExt3() == null : this.getExt3().equals(other.getExt3()))
@ -271,6 +282,7 @@ public class HighUser extends UserModel implements Serializable {
result = prime * result + ((getOpenId() == null) ? 0 : getOpenId().hashCode()); result = prime * result + ((getOpenId() == null) ? 0 : getOpenId().hashCode());
result = prime * result + ((getOpenIdH5() == null) ? 0 : getOpenIdH5().hashCode()); result = prime * result + ((getOpenIdH5() == null) ? 0 : getOpenIdH5().hashCode());
result = prime * result + ((getUnionId() == null) ? 0 : getUnionId().hashCode()); result = prime * result + ((getUnionId() == null) ? 0 : getUnionId().hashCode());
result = prime * result + ((getInfoCompleteStatus() == null) ? 0 : getInfoCompleteStatus().hashCode());
result = prime * result + ((getExt1() == null) ? 0 : getExt1().hashCode()); result = prime * result + ((getExt1() == null) ? 0 : getExt1().hashCode());
result = prime * result + ((getExt2() == null) ? 0 : getExt2().hashCode()); result = prime * result + ((getExt2() == null) ? 0 : getExt2().hashCode());
result = prime * result + ((getExt3() == null) ? 0 : getExt3().hashCode()); result = prime * result + ((getExt3() == null) ? 0 : getExt3().hashCode());
@ -297,6 +309,7 @@ public class HighUser extends UserModel implements Serializable {
sb.append(", openId=").append(openId); sb.append(", openId=").append(openId);
sb.append(", openIdH5=").append(openIdH5); sb.append(", openIdH5=").append(openIdH5);
sb.append(", unionId=").append(unionId); sb.append(", unionId=").append(unionId);
sb.append(", infoCompleteStatus=").append(infoCompleteStatus);
sb.append(", ext1=").append(ext1); sb.append(", ext1=").append(ext1);
sb.append(", ext2=").append(ext2); sb.append(", ext2=").append(ext2);
sb.append(", ext3=").append(ext3); sb.append(", ext3=").append(ext3);

@ -1012,6 +1012,66 @@ public class HighUserExample {
return (Criteria) this; return (Criteria) this;
} }
public Criteria andInfoCompleteStatusIsNull() {
addCriterion("info_complete_status is null");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusIsNotNull() {
addCriterion("info_complete_status is not null");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusEqualTo(Integer value) {
addCriterion("info_complete_status =", value, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusNotEqualTo(Integer value) {
addCriterion("info_complete_status <>", value, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusGreaterThan(Integer value) {
addCriterion("info_complete_status >", value, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusGreaterThanOrEqualTo(Integer value) {
addCriterion("info_complete_status >=", value, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusLessThan(Integer value) {
addCriterion("info_complete_status <", value, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusLessThanOrEqualTo(Integer value) {
addCriterion("info_complete_status <=", value, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusIn(List<Integer> values) {
addCriterion("info_complete_status in", values, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusNotIn(List<Integer> values) {
addCriterion("info_complete_status not in", values, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusBetween(Integer value1, Integer value2) {
addCriterion("info_complete_status between", value1, value2, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andInfoCompleteStatusNotBetween(Integer value1, Integer value2) {
addCriterion("info_complete_status not between", value1, value2, "infoCompleteStatus");
return (Criteria) this;
}
public Criteria andExt1IsNull() { public Criteria andExt1IsNull() {
addCriterion("ext_1 is null"); addCriterion("ext_1 is null");
return (Criteria) this; return (Criteria) this;

@ -156,6 +156,14 @@ public class HighUserServiceImpl implements HighUserService {
user.setName(highUser.getName()); user.setName(highUser.getName());
user.setOpenId(highUser.getOpenId()); user.setOpenId(highUser.getOpenId());
user.setUnionId(highUser.getUnionId()); user.setUnionId(highUser.getUnionId());
// 用户信息完整状态
// 0:完整
// 1:需要填写用户信息
// 2:需要填写手机号
if (user.getInfoCompleteStatus() == 2) {
user.setInfoCompleteStatus(0);
}
highUserMapper.updateByPrimaryKey(user); highUserMapper.updateByPrimaryKey(user);
return user; return user;
} }

Loading…
Cancel
Save