智子商城项目实践开发文档
创始人
2024-04-30 11:29:44
0

ZutShop

Junior practical training project

  • 在csdn暂存一下开发文档

本人大三做的实训项目,前后端分离。
后端:https://github.com/roydonGuo/ZutShop
前端使用Vue,前端项目地址:https://github.com/roydonGuo/ZutShop-Vue


0. 项目介绍

项目名称:智子商城。类似于购物车系统、订单系统。前后端分离。

image-20221220212423993

分析项目:

当开发某一个项目时,分许该项目中需要处理哪些数据?

例如:用户,收货地址,商品类别,商品,收藏,购物车,订单。。。。

处理数据的先后顺序:先处理基础数据,在处理相关数据,

例如需要先处理商品数据,才可以处理订单数据

处理以上数据的先后顺序:用户>收获地址>商品类别>商品>收藏>购物车>订单

当确定了数据处理顺序之后应该分析每个数据对应的功能有哪些

例如用户数据:注册,登录,修改密码,修改资料,上传头像。。。。

确定开发功能的开发顺序,遵循增>查>删>改

用户数据功能的开发顺序:注册>登录>修改密码>修改资料>上传头像。。。。

确定每一个功能的开发步骤:创建数据表>创建实体类>持久层>业务层>控制器层>前端页面

1. 数据库创建

关于项项目的数据表问题:

  1. 根据虚拟的前端页面和业务需求创建出各种数据表
  2. 画出各种数据表之间的关系图
  3. 每个表中的每个字段都应该有其作用否则将直接去掉

针对本项目,数据库设计如下:

image-20221222214609270

sql文件在项目resources中。

2. 项目准备

本项目使用springboot框架进行后台开发,前端使用Vue开发,axios进行前后端交互。所以,涉及到的技术栈有:

  • springboot
  • springSecurity
  • maven
  • redis
  • mybatis-plus
  • Vue,axios

2.1 创建maven项目

父工程管理依赖:

1.8UTF-888

org.springframework.bootspring-boot-dependencies2.5.0pomimportcom.alibabafastjson1.2.33io.jsonwebtokenjjwt0.9.0com.baomidoumybatis-plus-boot-starter3.5.2io.springfoxspringfox-swagger22.9.2io.springfoxspringfox-swagger-ui2.9.2

子工程Zut-Framework

org.springframework.bootspring-boot-starter-weborg.projectlomboklomboktrueorg.springframework.bootspring-boot-starter-testorg.springframework.bootspring-boot-starter-data-redisorg.springframework.bootspring-boot-starter-security2.5.14com.alibabafastjsonio.jsonwebtokenjjwtcom.baomidoumybatis-plus-boot-startermysqlmysql-connector-javaorg.springframework.bootspring-boot-starter-aopio.springfoxspringfox-swagger2io.springfoxspringfox-swagger-ui

子工程Zut-Shopping依赖于子工程Zut-FrameWork

edu.zutZut-Framework1.0.0

2.2 Zut-Shopping

Zut-Shopping为业务控制层,需要一个启动类:

并添加mapper包扫描和开启swagger

@SpringBootApplication
@MapperScan("edu.zut.mapper")
@EnableSwagger2
public class ShopApplication {public static void main(String[] args) {SpringApplication.run(ShopApplication.class, args);}
}

配置文件:

配置服务端口号7777,数据库信息,配置redis和MP。

server:port: 7777
spring:datasource:url: jdbc:mysql://localhost:3306/zut-shop?characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: qwer1234driver-class-name: com.mysql.cj.jdbc.Driver

2.3 Zut-Framework

Zut-Framework负责业务接口的实现。

新建User实体类:

IDEA有插件easycode快速生成MP代码,或者使用官方提供的代码生成器。

@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")
public class User {//用户id@TableIdprivate Integer uid;//用户名@ApiModelProperty(value = "用户名")private String username;//密码private String password;//性别,0-女,1-男private Integer gender;//电话private String phone;//邮箱private String email;//头像private String avatar;//是否删除,0-未删除,1-已删除private Integer isDelete;//创建执行人private Integer createdUser;//创建时间private Date createdTime;//修改执行人private Integer modifiedUser;//修改时间private Date modifiedTime;
}

UserMapper接口:

/*** Author: roydon - 2022/12/12**/
public interface UserMapper extends BaseMapper {}

Service层UserService:

public interface UserService extends IService {}

UserService的实现类UserServiceImpl

@Slf4j
@Service("userService")
public class UserServiceImpl extends ServiceImpl implements UserService {}

新建数据相应类ResponseResult,表示后端响应给前端的数据。

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult implements Serializable {private Integer code;private String msg;private T data;public ResponseResult() {this.code = AppHttpCodeEnum.SUCCESS.getCode();this.msg = AppHttpCodeEnum.SUCCESS.getMsg();}public ResponseResult(Integer code, T data) {this.code = code;this.data = data;}public ResponseResult(Integer code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}public ResponseResult(Integer code, String msg) {this.code = code;this.msg = msg;}public static ResponseResult errorResult(int code, String msg) {ResponseResult result = new ResponseResult();return result.error(code, msg);}public static ResponseResult okResult() {ResponseResult result = new ResponseResult();return result;}public static ResponseResult okResult(int code, String msg) {ResponseResult result = new ResponseResult();return result.ok(code, null, msg);}public static ResponseResult okResult(Object data) {ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());if (data != null) {result.setData(data);}return result;}public static ResponseResult errorResult(AppHttpCodeEnum enums) {return setAppHttpCodeEnum(enums, enums.getMsg());}public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg) {return setAppHttpCodeEnum(enums, msg);}public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums) {return okResult(enums.getCode(), enums.getMsg());}private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg) {return okResult(enums.getCode(), msg);}public ResponseResult error(Integer code, String msg) {this.code = code;this.msg = msg;return this;}public ResponseResult ok(Integer code, T data) {this.code = code;this.data = data;return this;}public ResponseResult ok(Integer code, T data, String msg) {this.code = code;this.data = data;this.msg = msg;return this;}public ResponseResult ok(T data) {this.data = data;return this;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}
}

新建一个枚举类,封装响应码对应响应信息。

public enum AppHttpCodeEnum {// 成功SUCCESS(200, "操作成功"),// 登录NEED_LOGIN(401, "需要登录后操作"),NO_OPERATOR_AUTH(403, "无权限操作"),USER_IS_DELETED(404, "用户已被删除"),SYSTEM_ERROR(500, "出现错误"),USERNAME_EXIST(501, "用户名已存在"),PHONENUMBER_EXIST(502, "手机号已存在"),EMAIL_EXIST(503, "邮箱已存在"),REQUIRE_USERNAME(504, "必需填写用户名"),CONTENT_NOT_NULL(506, "评论内容不能为空"),FILE_TYPE_ERROR(507, "文件类型错误,请上传png文件"),USERNAME_NOT_NULL(508, "用户名不能为空"),NICKNAME_NOT_NULL(509, "昵称不能为空"),PASSWORD_NOT_NULL(510, "密码不能为空"),PASSWORD_NOT_MATCH(513, "原密码错误"),EMAIL_NOT_NULL(511, "邮箱不能为空"),NICKNAME_EXIST(512, "昵称已存在"),LOGIN_ERROR(505, "用户名或密码错误");int code;String msg;AppHttpCodeEnum(int code, String errorMessage) {this.code = code;this.msg = errorMessage;}public int getCode() {return code;}public String getMsg() {return msg;}
}

跟redis的key相关字符串的封装:

public class RedisConstants {public static final String LOGIN_USER_KEY="login:user:";
}

一些需要用到的常量的封装:

public class SystemConstants {/*** 正常状态*/public static final String NORMAL = "0";public static final String ADMAIN = "1";/*** 用户是否为删除状态*/public static final Integer IS_DELETED = 1;public static final Integer IS_ALIVE = 0;}

2.4 MP配置

配置MP分页器。这个功能也许用不到,针对数据多的时候分页查询会减小很多额外的性能消耗。

@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());return mybatisPlusInterceptor;}
}

分析数据表t_user可知,当我们在crud时,同时需要修改创建时间、修改时间、修改人、创建人等字段。

例如下面场景:修改用户密码后,实现类中还需要单独设置修改人、获取系统时间对修改时间字段进行设置。麻烦不说,写起来十分别扭不美观。所以,解决方法MP已经为我们想到了,只需要实现MetaObjectHandler接口即可。每当调用MP提供的相应方法,下面实现类对应的方法就会执行。

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {Integer userId = null;try {userId = SecurityUtils.getUserId();} catch (Exception e) {e.printStackTrace();userId = -1;//表示是自己创建}this.setFieldValByName("createdTime", new Date(), metaObject);this.setFieldValByName("createdUser",userId , metaObject);this.setFieldValByName("modifiedTime", new Date(), metaObject);this.setFieldValByName("modifiedUser", userId, metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {this.setFieldValByName("modifiedTime", new Date(), metaObject);this.setFieldValByName("modifiedUser", SecurityUtils.getUserId(), metaObject);}
}

同时,User实体类要添加@TableField注解,添加规则

@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")
public class User {//用户id@TableIdprivate Integer uid;//用户名@ApiModelProperty(value = "用户名")private String username;//密码private String password;//性别,0-女,1-男private Integer gender;//电话private String phone;//邮箱private String email;//头像private String avatar;//是否删除,0-未删除,1-已删除private Integer isDelete;//创建执行人@TableField(fill = FieldFill.INSERT)//插入时触发handlerprivate Integer createdUser;//创建时间@TableField(fill = FieldFill.INSERT)private Date createdTime;//修改执行人@TableField(fill = FieldFill.INSERT_UPDATE)//更新时触发handlerprivate Integer modifiedUser;//修改时间@TableField(fill = FieldFill.INSERT_UPDATE)private Date modifiedTime;
}

配置文件添加MP配置:

server:port: 7777spring:datasource:url: jdbc:mysql://localhost:3306/zut-shop?characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: qwer1234driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql打印global-config:db-config:logic-delete-field: delFlaglogic-delete-value: 1logic-not-delete-value: 0id-type: auto

注意:MP配置好后最好在测试类中测试能否拿到数据库数据。

2.5 Redis配置

需要用到ali的fastjson序列化器在redis存值的时候进行序列化,保证是转义后的中文字符。

FastJsonRedisSerializer

/*** Redis使用FastJson序列化*/
public class FastJsonRedisSerializer implements RedisSerializer {public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;private Class clazz;static {ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(Class clazz) {super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null) {return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException {if (bytes == null || bytes.length <= 0) {return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}protected JavaType getJavaType(Class clazz) {return TypeFactory.defaultInstance().constructType(clazz);}
}

RedisConfig

@Configuration
public class RedisConfig {@Bean@SuppressWarnings(value = {"unchecked", "rawtypes"})public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}
}

redis操作封装工具类,来自ruoyi:

@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {@Resourcepublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key   缓存的键值* @param value 缓存的值*/public  void setCacheObject(final String key, final T value) {redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key      缓存的键值* @param value    缓存的值* @param timeout  时间* @param timeUnit 时间颗粒度*/public  void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间** @param key     Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout) {return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** @param key     Redis键* @param timeout 超时时间* @param unit    时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit) {return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public  T getCacheObject(final String key) {ValueOperations operation = redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象** @param key*/public boolean deleteObject(final String key) {return redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public long deleteObject(final Collection collection) {return redisTemplate.delete(collection);}/*** 缓存List数据** @param key      缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public  long setCacheList(final String key, final List dataList) {Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public  List getCacheList(final String key) {return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key     缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public  BoundSetOperations setCacheSet(final String key, final Set dataSet) {BoundSetOperations setOperation = redisTemplate.boundSetOps(key);Iterator it = dataSet.iterator();while (it.hasNext()) {setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/public  Set getCacheSet(final String key) {return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public  void setCacheMap(final String key, final Map dataMap) {if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public  Map getCacheMap(final String key) {return redisTemplate.opsForHash().entries(key);}/*** 根据map的key对值进行自增操作** @param key* @param hKey* @param v*/public void incrementCacheMapValue(String key, String hKey, long v) {redisTemplate.boundHashOps(key).increment(hKey, v);}/*** 往Hash中存入数据** @param key   Redis键* @param hKey  Hash键* @param value 值*/public  void setCacheMapValue(final String key, final String hKey, final T value) {redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** @param key  Redis键* @param hKey Hash键* @return Hash中的对象*/public  T getCacheMapValue(final String key, final String hKey) {HashOperations opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 删除Hash中的数据** @param key* @param hkey*/public void delCacheMapValue(final String key, final String hkey) {HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hkey);}/*** 获取多个Hash中的数据** @param key   Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public  List getMultiCacheMapValue(final String key, final Collection hKeys) {return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection keys(final String pattern) {return redisTemplate.keys(pattern);}
}
 

配置文件添加redis配置:我的redis没有设置密码,省略密码配置

server:port: 7777spring:datasource:url: jdbc:mysql://localhost:3306/zut-shop?characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: qwer1234driver-class-name: com.mysql.cj.jdbc.Driverredis:host: 127.0.0.1port: 6379database: 7
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:logic-delete-field: delFlaglogic-delete-value: 1logic-not-delete-value: 0id-type: auto

2.6 Swagger配置

配Swagger置写在shopping模块

  • RequestHandlerSelectors.basePackage(“edu.zut.controller”)中填写控制层的包
  • ApiInfo可以根据自己需要自行配置
@Configuration
public class SwaggerConfig {@Beanpublic Docket customDocket() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("edu.zut.controller")).build();}private ApiInfo apiInfo() {Contact contact = new Contact("roydon", "https://www.roydon.top", "3133010060@qq.com");return new ApiInfoBuilder().title("zut-shop").description("智子商城").contact(contact)   // 联系方式.version("1.0.0")  // 版本.build();}
}

启动类添加注解@EnableSwagger2启动Swagger

项目启动访问http://localhost:7777/swagger-ui.html即可

具体使用可参考官方网站,或前往:http://c.biancheng.net/view/5532.html

image-20221213205613086

2.7 SpringSecurity配置

Shopping模块中新建类SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and()//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问 anonymous.antMatchers("/user/login").anonymous().anyRequest().authenticated();}/*** AuthenticationManager注册进容器** @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}

配置中注入好Security使用的加密工具BCryptPasswordEncoder后在测试类中测试密码加密功能是否正常。

此处引入spring容器管理的bean,重新new BCryptPasswordEncoder密码会匹配不上。

@Resource
private PasswordEncoder passwordEncoder;@Test
void pwdTest() {String pwd = passwordEncoder.encode("123456");System.out.println(pwd);
}

比较原始密码与加密密码,结果返回Boolean类型

boolean matches = passwordEncoder.matches("123456",user.getPassword()); //true

3. 用户业务

3.1 登录登出接口

3.1.1登录

controller

@RestController
@RequestMapping("/user")
public class LoginController {@Resourceprivate LoginService loginService;@PostMapping("/login")@ApiOperation(value = "用户登录")public ResponseResult login(@RequestBody User user){if(!StringUtils.hasText(user.getUsername())){//提示 必须要传用户名throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);}return loginService.login(user);}}

LoginService

public interface LoginService {ResponseResult login(User user);ResponseResult logout();
}

LoginUser实体类,封装进User和用户的权限,权限本系统不需要,直接跳过返回null

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {private User user;@Overridepublic Collection getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

实现UserDetailsService接口,重写loadUserByUsername()方法。暂且不需要角色权限。返回LoginUser对象

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Resourceprivate UserMapper userMapper;@Override@Transactionalpublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();// 方法引用queryWrapper.eq(StringUtils.isNotEmpty(username),User::getUsername,username);User user = userMapper.selectOne(queryWrapper);if (Objects.isNull(user)) {throw new UsernameNotFoundException("用户名或密码错误");}//判断用户是否被删除if (Objects.equals(user.getIsDelete(), IS_DELETED)) {throw new SystemException(AppHttpCodeEnum.USER_IS_DELETED);}log.info("数据库登录用户:{}",user);//TODO 查询角色权限return new LoginUser(user);}}

service实现类:

@Slf4j
@Service
public class LoginServiceImpl implements LoginService {@Resourceprivate AuthenticationManager authenticationManager;@Resourceprivate RedisCache redisCache;/*** 登录* @param user* @return ResponseResult.okResult(userLoginVo)*/@Overridepublic ResponseResult login(User user) {//判断用户名是否为空if (StringUtils.isEmpty(user.getUsername())) {throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);}UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());Authentication authentication = authenticationManager.authenticate(authenticationToken);//判断是否认证通过if (Objects.isNull(authentication)) {throw new RuntimeException("用户名或密码错误");}// 认证成功,从Authentication获取LoginUserLoginUser loginUser = (LoginUser) authentication.getPrincipal();log.info("loginUser:{}", loginUser);String userId = loginUser.getUser().getUid().toString();// 生成tokenString jwt = JwtUtil.createJWT(userId);// 存入redisredisCache.setCacheObject(LOGIN_USER_KEY + userId, loginUser);UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);UserLoginVo userLoginVo = new UserLoginVo(jwt, userInfoVo);log.info("用户以登陆==>{}", userLoginVo);return ResponseResult.okResult(userLoginVo);}
}

封装的前后端交互实体UserInfoVo,把重要的信息隐藏起来。

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class UserInfoVo {/*** 主键*/private Integer uid;/*** 用户名*/private String username;/*** 手机号*/private String phone;/*** 头像*/private String avatar;private Integer gender;private String email;}

相应给前端token实体类UserLoginVo

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginVo {private String token;private UserInfoVo userInfo;
}

自定义异常:前文枚举类已经定义过,就是此处构造方法参数AppHttpCodeEnum,当服务需要往外抛出错误时,指定响应码和对应响应信息,threw new SystemException(),把枚举AppHttpCodeEnum传递进去即可。

public class SystemException extends RuntimeException {private int code;private String msg;public int getCode() {return code;}public String getMsg() {return msg;}public SystemException(AppHttpCodeEnum httpCodeEnum) {super(httpCodeEnum.getMsg());this.code = httpCodeEnum.getCode();this.msg = httpCodeEnum.getMsg();}
}

全局异常捕获handler:

@RestControllerAdvice这个注解继承了多个注解,包括Compont注解,作用之一就是可以自定义异常信息。当捕获到异常响应给且前端。

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(SystemException.class)public ResponseResult systemExceptionHandle(SystemException se) {log.info("最喜欢异常了==>{}", se);return ResponseResult.errorResult(se.getCode(), se.getMessage());}@ExceptionHandler(Exception.class)public ResponseResult exceptionHandle(Exception e) {log.info("最喜欢异常了==>{}", e);return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(), e.getMessage());}
}

jwt认证过滤器:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Resourceprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取请求头中的tokenString token = request.getHeader("token");if (StringUtils.isEmpty(token)) {filterChain.doFilter(request, response);return;}//解析获取useridClaims claims = null;try {claims = JwtUtil.parseJWT(token);} catch (Exception e) {// token超时,或token非法e.printStackTrace();
//            ResponseResult responseResult = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
//            WebUtils.renderString(response, JSON.toJSONString(responseResult));return;}String userId = claims.getSubject();//从redis中获取用户信息LoginUser loginUser = redisCache.getCacheObject(LOGIN_USER_KEY + userId);//如果redis获取不到if (Objects.isNull(loginUser)) {throw new RuntimeException("用户未登录");}//存入SecurityContextHolder//TODO 获取权限信息封装到 Authentication 中UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}}

配置类添加jwt过滤器规则

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Resourceprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable();// 关闭csrfhttp//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口、注册接口 允许匿名访问.antMatchers("/user/login").anonymous()//注销接口需要认证才能访问.antMatchers("/user/logout").authenticated()// 除上面外的所有请求全部不需要认证即可访问.anyRequest().permitAll();// jwt过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//关闭默认的注销功能http.logout().disable();//允许跨域http.cors();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}

Apifox测试登录接口:

image-20221213214122124

redis也成功存入数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x90fbYvo-1671935951973)(https://raw.githubusercontent.com/roydonGuo/Typora-Pic/main/md-pic202212211736944.png)]

3.1.2 登出

controller接口

@RequestMapping("/logout")
@ApiOperation(value = "退出登录")
public ResponseResult logout(){return loginService.logout();
}

实现类重写登出方法

/*** 退出登录* 1.获取用户信息 SecurityContextHolder.getContext().getAuthentication();* 2.通过用户 id 清除 redis** @return ResponseResult(CODE_200, " 退出成功 ");*/
@Override
public ResponseResult logout() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();Integer uid = loginUser.getUser().getUid();redisCache.deleteObject(LOGIN_USER_KEY + uid);return new ResponseResult(CODE_200, "退出成功");
}

3.2 注册用户

前端页面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECOvEIpx-1671935951974)(https://raw.githubusercontent.com/roydonGuo/Typora-Pic/main/md-pic202212211736151.png)]

3.2.1 需求分析

前端发送ajax请求,确认密码由前端进行判断,前端只传给后端用户名、密码两个字段的json串。

  • 前端传入数据格式:(json){ ‘username’: username, ‘password’: password }

  • 请求地址:/uesr/register

3.2.2 后端实现

①. UserController写一个注册接口

@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate UserService userService;@PostMapping("/register")public ResponseResult register(@RequestBody UserDto userDto) {return ResponseResult.okResult(userService.register(userDto));}}

②. 前端传入对象封装

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "前端dto")
public class UserDto {//用户名private String username;//原密码private String password;//新密码private String newPassword;
}

③. 实现类重写接口中注册方法

@Slf4j
@Service("userService")
public class UserServiceImpl extends ServiceImpl implements UserService {@Resourceprivate RedisCache redisCache;@Resourceprivate PasswordEncoder passwordEncoder;@Overridepublic ResponseResult register(UserDto userDto) {//非空判断if (StringUtils.isEmpty(userDto.getUsername())) {throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);}if (StringUtils.isEmpty(userDto.getPassword())) {throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);}//数据库中是否存在此用户名if (userNameExist(userDto.getUsername())) {throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);}//密码加密String encodePassword = passwordEncoder.encode(userDto.getPassword());userDto.setPassword(encodePassword);save(BeanCopyUtils.copyBean(userDto,User.class));return ResponseResult.okResult();}private boolean userNameExist(String userName) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUsername, userName);return count(queryWrapper) > 0;}}

④. 对象拷贝工具封装

因为前端传过来的是UserDto,但执行sql需要用到数据表对应实体:User,所以封装一个对象拷贝工具转一下对象后再进行crud操作。

/*** 对象、集合拷贝工具类*/
public class BeanCopyUtils {private BeanCopyUtils() {}public static  V copyBean(Object source, Class clazz) {//创建目标对象V result = null;try {result = clazz.newInstance();BeanUtils.copyProperties(source, result);} catch (Exception e) {e.printStackTrace();}return result;}public static  List copyBeanList(List list, Class clazz) {return list.stream().map(o -> copyBean(o, clazz)).collect(Collectors.toList());}
}

3.3 修改密码

前端页面:

image-20221220212607942

3.3.1 需求分析

前端提供表单,表单内容包括用户名,原密码和新密码。在前端还会进行确认新密码的校验工作。当然,校验只放在前端即可。

image-20221221173833197

  • 前端传入数据格式:

{

​ “username”: “roydon”,

​ “password”: “123456”,

​ “confirmPassword”: “111111”

}

发现前端传入数据与后端的User实体类字段数量相差甚远,甚至还有后端没有的字段。这就需要后端设计一个新的用于前后端进行数据传输的对象(DTO)命名为UserDto

  • 请求地址:post("/user/password")

3.3.2 后端实现

①新建UserDto实体类用来接收前端传入的数据,他屏蔽了大量的用户隐私信息。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "注册用户dto")
public class UserDto {//用户名private String username;//原密码private String password;//新密码private String newPassword;}

②控制层UserController编写一个接口

/*** 修改密码** @param userDto* @return*/
@PostMapping("/password")
public ResponseResult update(@RequestBody UserDto userDto) {return ResponseResult.okResult(userService.updatePwd(userDto));
}

③业务层实现修改密码方法

ResponseResult updatePwd(UserDto userDto);
@Override
public ResponseResult updatePwd(UserDto userDto) {//非空判断if (StringUtils.isEmpty(userDto.getPassword())) {throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);}//取出登录用户的idInteger userId =null;try {userId = SecurityUtils.getUserId();}catch (Exception e) {//未登录throw new SystemException(NEED_LOGIN);}if(Objects.isNull(userId)){//没有携带tokenthrow new SystemException(AppHttpCodeEnum.NO_OPERATOR_AUTH);}//查询用户LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUid, userId);User one = getOne(queryWrapper);//判断输入密码是否与数据库相同boolean matches = passwordEncoder.matches(userDto.getPassword(),one.getPassword());if (!matches) {//不存在用户throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_MATCH);}//从redis中获取用户信息LoginUser loginUser = redisCache.getCacheObject(LOGIN_USER_KEY + userId);//如果redis获取不到if (Objects.isNull(loginUser)) {throw new RuntimeException("用户未登录");}//新密码加密String encodePassword = passwordEncoder.encode(userDto.getNewPassword());User user = loginUser.getUser();user.setPassword(encodePassword);log.info("修改后的用户:{}",user);redisCache.setCacheObject(LOGIN_USER_KEY + userId,new LoginUser(user) );return ResponseResult.okResult(update(user,queryWrapper));
}

3.4 更新用户信息

3.4.1 需求分析

image-20221221175300012

提供一个表单,即是展示用户信息的页面,也可直接进行修改,以及头像的上传。

  • 前端接口:post("/user/update")

3.4.2 后端实现

①控制层

/*** 更新用户信息** @param user* @return*/
@PostMapping("/update")
public ResponseResult updateUser(@RequestBody User user) {return ResponseResult.okResult(userService.updateUserInfo(user));
}

②业务层

ResponseResult updateUserInfo(User user);
@Override
public ResponseResult updateUserInfo(User user) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUid,user.getUid());redisCache.setCacheObject(LOGIN_USER_KEY + user.getUid(), new LoginUser(user));return ResponseResult.okResult(update(user,queryWrapper));
}

3.5 头像上传

3.5.1 需求分析

紧接着上面修改用户信息步骤,此次需求为修改用户头像。

  • 前端接口:http://localhost:7777/file/upload

3.5.2 后端实现

①头像存储使用七牛云的对象存储服务,前往官网https://portal.qiniu.com/kodo/bucket。

注册完用户并登录,打开控制台选择存储空间:

image-20221221180315679

选择新建空间,访问权限最好设置为公开,名称随意:

image-20221221180406779

创建好存储空间,七牛云会给此空间提供一个为期三十天的测试域名,也就是说,你上传了图片,可以根据·此测试域名和图片名称访问此网络图片。

点开右上角个人头像创建密钥。

image-20221221180918673

②项目引入依赖,在Framework工程引入依赖


com.qiniuqiniu-java-sdk[7.7.0, 7.7.99]

com.google.code.gsongson2.9.0

添加配置

oss: #七牛云对象存储accessKey: 5QiJ****************WX-_O3isecretKey: S6t****************FTkFWx51bucket: zut-shop-avatar # 空间名称

③控制层FileController

@PostMapping("/upload")
public ResponseResult uploadFile(@RequestParam MultipartFile file) {//头像上传return ResponseResult.okResult(fileService.uploadImg(file));
}

④业务层

ResponseResult uploadImg(MultipartFile file);
@Data
@Slf4j
@Service
//@ConfigurationProperties(prefix = "oss")
public class FileServiceImpl implements FileService {@Overridepublic ResponseResult uploadImg(MultipartFile file) {String originalFilename = file.getOriginalFilename();//对原始文件名进行判断
//        if(!originalFilename.endsWith(".png")){
//            throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
//        }//上传文件到OSSassert originalFilename != null;String filePath = PathUtils.generateFilePath(originalFilename);String url = uploadOss(file, filePath);log.info("图片上传地址:{}", url);return ResponseResult.okResult(url);}@Value("${oss.accessKey}")private String accessKey;@Value("${oss.secretKey}")private String secretKey;@Value("${oss.bucket}")private String bucket;private String uploadOss(MultipartFile imgFile, String filePath) {//构造一个带指定 Region 对象的配置类Configuration cfg = new Configuration(Region.autoRegion());//...其他参数参考类注释UploadManager uploadManager = new UploadManager(cfg);//默认不指定key的情况下,以文件内容的hash值作为文件名String key = filePath;try {InputStream inputStream = imgFile.getInputStream();Auth auth = Auth.create(accessKey, secretKey);String upToken = auth.uploadToken(bucket);try {Response response = uploadManager.put(inputStream, key, upToken, null, null);//解析上传成功的结果DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);System.out.println(putRet.key);System.out.println(putRet.hash);return "http://rm*******d-bkt.clouddn.com/" + key;//换成你自己的测试域名} catch (QiniuException ex) {Response r = ex.response;System.err.println(r.toString());try {System.err.println(r.bodyString());} catch (QiniuException ex2) {//ignore}}} catch (Exception ex) {//ignore}return "www";}}

4. 收货地址管理

4.1 增加收货地址

4.1.1 需求分析

image-20221221183956994

  • 前端传入:Address实体

  • 请求方式:post("/address/add")

4.1.2 后端实现

①控制层

/*** 新增收货地址** @param address* @return*/
@PostMapping("/add")
public ResponseResult addAddress(@RequestBody Address address) {return ResponseResult.okResult(addressService.addAddress(address));
}

②业务层

 @Override
public ResponseResult addAddress(Address address) {//取出登录用户的idInteger userId =  SecurityUtils.getUserId();LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Address::getUid, userId);//如果新增为第一条地址数据,则设为默认if (count(queryWrapper) < 1) {address.setIsDefault(IS_DEFAULT);}address.setUid(SecurityUtils.getUserId());save(address);return ResponseResult.okResult(); }

4.2 查询收货地址

4.2.1 需求分析

  • 前端传入分页参数:@RequestParam Integer pageNum, @RequestParam Integer pageSize
  • 请求地址:get("/address/page")

4.2.2 后端实现

① 控制层

/**
* 分页查询用户收货地址数据
*
* @param pageNum
* @param pageSize
* @return
*/
GetMapping("/page")
ublic ResponseResult selectAllAddress(@RequestParam Integer pageNum, @RequestParam Integer pageSize) {return ResponseResult.okResult(addressService.userAddressList(pageNum, pageSize));
}

② 业务层,前期已经配置好MP分页插件。

@Override
public ResponseResult userAddressList(Integer pageNum, Integer pageSize) {//取出登录用户的idInteger userId = SecurityUtils.getUserId();if (Objects.isNull(userId)) {//没有携带tokenthrow new SystemException(AppHttpCodeEnum.NO_OPERATOR_AUTH);}LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Address::getUid, userId);Page
page = page(new Page<>(pageNum, pageSize), queryWrapper);return ResponseResult.okResult(page); }

4.3 编辑收货地址

4.3.1 需求分析

image-20221221184024156

  • 前端传入Address实体
  • 请求地址:post("/address/update")

4.3.2 后端实现

①控制层

/*** 更新地址** @param address* @return*/@PostMapping("/update")public ResponseResult update(@RequestBody Address address) {return ResponseResult.okResult(addressService.updateAddress(address));}

②业务层

@Override
public ResponseResult updateAddress(Address address) {LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Address::getAid, address.getAid());update(address, queryWrapper);return ResponseResult.okResult(); }

4.4 删除收货地址

4.4.1 需求分析

前端传入地址id,后端根据地址id删除

  • 请求地址:delete("/address/{aid}")

4.4.2 后端实现

①控制层直接调用构造器删除

 /*** 删除地址数据** @param aid* @return*/
@DeleteMapping("/{aid}")
public ResponseResult delete(@PathVariable Integer aid) {LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Address::getAid, aid);return ResponseResult.okResult(addressService.remove(queryWrapper)); }

4.5 收货地址设为默认

4.5.1 需求分析

当购买商品准备结算时,会有选择收货地址选项,设置默认收货地址下次购物直接忽略选择地址这一选项方便用户操作,优化用户体验。

  • 前端传入数据格式为Address实体类
  • 请求接口:post("/address/setDefault")

4.5.2 后端实现

①控制层

/*** 设为默认** @param address* @return*/
@PostMapping("/setDefault")
public ResponseResult setDefault(@RequestBody Address address) {return ResponseResult.okResult(addressService.setDefaultAddress(address));
}

②业务层

此处需要多次操作数据库,加个@Transactional注解,自动回滚事务,保证数据库执行此方法的前后一致性。

@Override
@Transactional
public ResponseResult setDefaultAddress(Address address) {LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>();//地址默认改为非默认if (address.getIsDefault().equals(NOT_DEFAULT)) {address.setIsDefault(NOT_DEFAULT);queryWrapper.eq(Address::getAid, address.getAid());update(address, queryWrapper);return ResponseResult.okResult();}//先全部改为默认状态queryWrapper.eq(Address::getUid, SecurityUtils.getUserId());//用户的全部地址数据List
addressList = list(queryWrapper);//过滤出默认地址,理论为一个List
collect = addressList.stream().filter(a ->a.getIsDefault().equals(IS_DEFAULT)).collect(Collectors.toList());collect.forEach(a -> {a.setIsDefault(NOT_DEFAULT);queryWrapper.eq(Address::getAid, a.getAid());update(a, queryWrapper);});//最后将选择修改的非默认地址改为默认LambdaQueryWrapper
queryWrapper2 = new LambdaQueryWrapper<>();queryWrapper2.eq(Address::getAid, address.getAid());address.setIsDefault(IS_DEFAULT);update(address, queryWrapper2);return ResponseResult.okResult(); }

5. 首页完善

image-20221221184202762

5.1 添加今日热销栏

5.1.1 需求分析

  • 前端负责渲染,后端只需根据商品表的修改时间字段进行查找,并制定查找条数。

5.1.2 后端实现

①控制层

/*** 今日热销** @param pageNum* @param pageSize* @return*/
@GetMapping("/today")
public ResponseResult todayGood(@RequestParam Integer pageNum,@RequestParam Integer pageSize) {return ResponseResult.okResult(goodsService.todayGoodList(pageNum, pageSize));
}

②业务层

@Override
public Page todayGoodList(Integer pageNum, Integer pageSize) {//查询redisPage todayGoodsList = redisCache.getCacheObject(TODAY_GOODS_KEY + pageNum);if (todayGoodsList == null) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();//按创建时间倒序排序queryWrapper.orderByDesc(Goods::getCreatedTime);Page goodsPage = page(new Page<>(pageNum, pageSize), queryWrapper);//TODO 存入redisredisCache.setCacheObject(TODAY_GOODS_KEY + pageNum, goodsPage, TODAY_GOODS_TTL, TimeUnit.MINUTES);return goodsPage;} else {return todayGoodsList;}
}

5.2 商品展示页

5.2.1 需求分析

image-20221221184759925

分页查询出商品及和,前端负责渲染

5.2.2 后端实现

①控制层

/*** 分页查询所有商品【暂行方案】** @param pageNum* @param pageSize* @return*/
@GetMapping("/list")
public ResponseResult selectAll(@RequestParam Integer pageNum, @RequestParam Integer pageSize) {return ResponseResult.okResult(goodsService.goodList(pageNum, pageSize));
}

②业务层

@Override
public ResponseResult goodList(Integer pageNum, Integer pageSize) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();//按优先级排序queryWrapper.orderByDesc(Goods::getPriority);Page page = page(new Page<>(pageNum, pageSize));return ResponseResult.okResult(page);
}

5.3 商品搜索

5.3.1 需求分析

前端在输入框输入商品名称,后端根据输入名称进行模糊查询并返回商品集合。

5.3.2 后端实现

①控制层

 /*** 根据商品名模糊搜索,并分页** @param title 商品名* @return ResponseResult*/
@GetMapping("/search")
public ResponseResult searchGood(@RequestParam Integer pageNum,@RequestParam Integer pageSize,@RequestParam String title) {return ResponseResult.okResult(goodsService.searchGoodListByTitle(pageNum, pageSize, title));
}

②业务层

@Override
public Page searchGoodListByTitle(Integer pageNum, Integer pageSize, String title) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.like(Goods::getTitle, title);//按优先级排序queryWrapper.orderByDesc(Goods::getPriority);Page goodsPage = page(new Page<>(pageNum, pageSize), queryWrapper);return goodsPage;
}

5.4 商品收藏

5.4.1 需求分析

点击商品页加入收藏按钮添加到我的收藏中。

5.4.2 后端实现

①控制层

/*** 添加商品收藏** @param gid* @return*/
@PostMapping("/add")
public ResponseResult add(@RequestBody Integer gid) {return ResponseResult.okResult(favoritesService.addFavorites(gid));
}

②业务层

@Override
public boolean addFavorites(Integer gid) {//取出登录用户的idInteger userId = SecurityUtils.getUserId();Favorites favorites = new Favorites();favorites.setGid(gid);favorites.setUid(userId);boolean saveOrUpdate = saveOrUpdate(favorites);return saveOrUpdate;
}

6. 订单管理

image-20221221185417592

6.1 查询订单列表

6.1.1 需求分析

查询出用户的所有订单,并根据订单的oid关联查询订单包含的商品集合。

此时需要一个实体类封装传输数据既包含订单信息,也包含订单商品集合信息。

6.1.2 后端实现

①新建OrderGoodVo实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderGoodVo {private Integer oid;//归属于那个用户private Integer uid;//收货人private String name;//下单时间private Date orderTime;//是否发货private Integer status;//集合private List orderItemList;
}

②控制层

/*** 分页查询当前登录用户的所有订单** @param pageNum pageNum* @param pageSize pageSize* @return ResponseResult*/
@GetMapping("/list")
public ResponseResult selectAll(@RequestParam Integer pageNum, @RequestParam Integer pageSize) {return ResponseResult.okResult(orderService.userOrderList(pageNum, pageSize));
}

③业务层

前端需要分页展示,所以还得封装Page分页对象

@Override
public ResponseResult userOrderList(Integer pageNum, Integer pageSize) {//取出登录用户的idInteger userId = null;try {userId = SecurityUtils.getUserId();} catch (Exception e) {//未登录throw new SystemException(NEED_LOGIN);}if (Objects.isNull(userId)) {//没有携带tokenthrow new SystemException(AppHttpCodeEnum.NO_OPERATOR_AUTH);}LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Order::getUid, userId);Page pageOrder = new Page<>(pageNum, pageSize);Page page = page(pageOrder, queryWrapper);List orderList = page.getRecords();//TODO 将订单包含的商品order_item封装进orderList orderGoodVoList = new ArrayList<>();orderList.forEach(o -> {//订单odInteger oid = o.getOid();//根据订单id查询order_item集合LambdaQueryWrapper queryWrapper2 = new LambdaQueryWrapper<>();queryWrapper2.eq(OrderItem::getOid, oid);List orderItemList = orderItemService.list(queryWrapper2);OrderGoodVo orderGoodVo = BeanCopyUtils.copyBean(o, OrderGoodVo.class);orderGoodVo.setOrderItemList(orderItemList);orderGoodVoList.add(orderGoodVo);});Page orderGoodVoPage = new Page<>();orderGoodVoPage.setCurrent(page.getCurrent());orderGoodVoPage.setPages(page.getPages());orderGoodVoPage.setSize(page.getSize());orderGoodVoPage.setTotal(page.getTotal());orderGoodVoPage.setRecords(orderGoodVoList);return ResponseResult.okResult(orderGoodVoPage);
}

6.2 查询订单商品详情

image-20221221190201548

6.2.1 需求分析

点击订单商品的查看详情按钮,跳转到订单详情页面,前端根据订单商品的gid查询并显示。

6.2.2 后端实现

①业务层

在商品控制层编写

 /*** 根据gid查询商品** @param gid* @return*/
@GetMapping("{gid}")
public ResponseResult getGoods(@PathVariable Integer gid) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Goods::getGid, gid);return ResponseResult.okResult(goodsService.getOne(queryWrapper));
}

6.3 确认收货

6.3.1 需求分析

当订单处于已支付状态,订单每个商品均显示确认收货按钮,用户点击后先删除订单关联的此商品,若订单没有再关联的商品则把订单删除或者设置状态为已签收。

6.3.2 后端实现

①控制层

 /*** 订单中的商品已签收** @param orderItem* @return*/
@PostMapping("/receive")
public ResponseResult remove(@RequestBody OrderItem orderItem) {return ResponseResult.okResult(orderItemService.delOrderItem(orderItem));
}

②业务层

@Override
@Transactional
public ResponseResult delOrderItem(OrderItem orderItem) {log.info("订单商品==>{}",orderItem);LambdaQueryWrapper orderItemLambdaQueryWrapper = new LambdaQueryWrapper<>();orderItemLambdaQueryWrapper.eq(OrderItem::getGid, orderItem.getGid());orderItemLambdaQueryWrapper.eq(OrderItem::getOid, orderItem.getOid());orderItemLambdaQueryWrapper.eq(OrderItem::getCreatedUser, SecurityUtils.getUserId());boolean remove = remove(orderItemLambdaQueryWrapper);//删除了订单中的商品,判断订单是否还有商品,没有则删除if (remove) {LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();wrapper.eq(OrderItem::getOid, orderItem.getOid());wrapper.eq(OrderItem::getCreatedUser, SecurityUtils.getUserId());List orderList = list(wrapper);//此订单已经没有商品if (orderList.size() == 0) {//删除订单LambdaQueryWrapper orderLambdaQueryWrapper = new LambdaQueryWrapper<>();orderLambdaQueryWrapper.eq(Order::getOid, orderItem.getOid());orderLambdaQueryWrapper.eq(Order::getUid, SecurityUtils.getUserId());boolean remove2 = orderService.remove(orderLambdaQueryWrapper);}}return ResponseResult.okResult();
}

7. 购物车管理

7.1 添加购物车

7.1.1 需求分析

image-20221221192506717

点击商品会跳转到商品详情页,当用户需要选择加入购物车时,同样可以选择加入购物车的数量。

如果购物车已经有了该商品,则在此数据的基础之上进行更新操作。

  • 前端传入数据格式:{gid: 100000391, num: 3}
  • 请求接口:post("/cart/add")

7.1.2 后端实现

①控制层

/*** 添加购物车** @param cart* @return*/
@PostMapping("/add")
public ResponseResult addCart(@RequestBody Cart cart) {return ResponseResult.okResult(cartService.addCartByUid(cart));
}

②业务层

@Override
public ResponseResult addCartByUid(Cart cart) {//取出登录用户的idInteger userId = SecurityUtils.getUserId();cart.setUid(userId);if(cart.getNum()==0||cart.getNum()==null){cart.setNum(1);}//如果购物车有了此商品,就更新LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Cart::getUid, userId);queryWrapper.eq(Cart::getGid, cart.getGid());log.info("待加入购物车数据==>{}", cart);saveOrUpdate(cart,queryWrapper);return ResponseResult.okResult();
}

7.2 查询购物车

7.2.1 需求分析

image-20221221193212323

跟据当前登录用户查询出其购物车数据即可。

  • 请求接口:get("/cart/list"),参数为用户uid

7.2.2 后端实现

①控制层

/*** 用户购物车数据** @param uid* @return*/
@RequestMapping("/list")
public ResponseResult userCart(@RequestParam Integer uid) {return ResponseResult.okResult(cartService.userCartGoodList(uid));
}

②业务层

@Override
public List userCartGoodList(Integer uid) {//先根据用户id查询购物车数据LambdaQueryWrapper cartLambdaQueryWrapper = new LambdaQueryWrapper<>();cartLambdaQueryWrapper.eq(Cart::getUid, uid);cartLambdaQueryWrapper.orderByDesc(Cart::getCreatedTime);List cartList = list(cartLambdaQueryWrapper);//封装VOList cartGoodsVoList = new ArrayList<>();cartList.forEach(c -> {CartGoodsVo cartGoodsVo = BeanCopyUtils.copyBean(c, CartGoodsVo.class);//商品idInteger gid = c.getGid();LambdaQueryWrapper goodsLambdaQueryWrapper = new LambdaQueryWrapper<>();goodsLambdaQueryWrapper.eq(Goods::getGid, gid);// TODO 待添加此商品是否被下架逻辑Goods goods = goodsService.getOne(goodsLambdaQueryWrapper);cartGoodsVo.setGoods(goods);cartGoodsVoList.add(cartGoodsVo);});return cartGoodsVoList;
}

7.3 删除购物车商品

gif_1ewf23fw

7.3.1 需求分析

前端点击删除按钮,购物车数据就会删除。

  • 请求接口:delete("/cart/"),请求参数为购物车cid

7.3.2 后端实现

①控制层

/*** 根据购物车id删除购物车** @param cid* @return*/
@DeleteMapping("/{cid}")
public ResponseResult deleteCart(@PathVariable Integer cid) {return ResponseResult.okResult(cartService.removeCartGoodByCid(cid));
}

②业务层

@Override
public boolean removeCartGoodByCid(Integer cid) {LambdaQueryWrapper cartLambdaQueryWrapper = new LambdaQueryWrapper<>();cartLambdaQueryWrapper.eq(Cart::getCid, cid);boolean remove = remove(cartLambdaQueryWrapper);return remove;
}

8. 整合支付宝支付(沙箱)

只是使用支付宝支付api进行交易模拟,需前往支付宝开发平台进行沙箱应用创建。

支付宝开发平台地址地址==>https://open.alipay.com/develop/sandbox/app

使用到的依赖:

org.springframework.bootspring-boot-starter

org.springframework.bootspring-boot-starter-web


org.projectlomboklomboktrue


mysqlmysql-connector-java


com.alipay.sdkalipay-easysdk2.2.0


com.baomidoumybatis-plus-boot-starter3.5.2

cn.hutoolhutool-all5.8.10

需要的实体:Alipay

@Data
public class AliPay {//订单号private String traceNo;//金额private String totalAmount;private String subject;
//    private String alipayTraceNo;
}

配置文件yml配置:

alipay:appId: 202100*******appPrivateKey: **************alipayPublicKey: *********notifyUrl: 

查看沙箱应用id、公钥和私钥补全配置。

alipay配置类用来加载配置信息:

@Data
@Slf4j
@Component
@ConfigurationProperties(prefix = "alipay")
public class AliPayConfig {private static final String GATEWAY_URL ="https://openapi.alipaydev.com/gateway.do";private static final String FORMAT ="JSON";private static final String CHARSET ="utf-8";private static final String SIGN_TYPE ="RSA2";private String appId;private String appPrivateKey;private String alipayPublicKey;private String notifyUrl;@PostConstructpublic void init() {// 设置参数(全局只需设置一次)Config config = new Config();config.protocol = "https";config.gatewayHost = "openapi.alipaydev.com";config.signType = SIGN_TYPE;config.appId = this.appId;config.merchantPrivateKey = this.appPrivateKey;config.alipayPublicKey = this.alipayPublicKey;config.notifyUrl = this.notifyUrl;Factory.setOptions(config);System.out.println(JSONUtil.toJsonStr(config));log.info("=======支付宝SDK初始化成功=======");}
}

控制层写一个发起支付接口:

/*** http://localhost:7778/alipay/pay?subject=15689585674&traceNo=1024253&totalAmount=3333** @param aliPay* @return*/
@GetMapping("/pay")
public String pay(AliPay aliPay) {AlipayTradePagePayResponse response;try {//  发起API调用(以创建当面付收款二维码为例)response = Factory.Payment.Page().pay(URLEncoder.encode(aliPay.getSubject(), "UTF-8"), aliPay.getTraceNo(), aliPay.getTotalAmount(), "");} catch (Exception e) {System.err.println("调用遭遇异常,原因:" + e.getMessage());throw new RuntimeException(e.getMessage(), e);}return response.getBody();
}

打开谷歌浏览器无痕窗口访问写好的支付地址自动跳转到支付页面:

image-20221222202104500

此时就可以模拟支付款了,账号密码在支付宝开发平台皆可查到。

那支付成功后,就必然有一个回调,但支付宝平台是公网,我们本地项目它回调不过来,所以暂且需要把本地端口暴露到公网,使用内网穿透工具==>https://natapp.cn/

注册登录可申请免费内网穿透,指定端口号后拿到官网给的密钥,下载内网穿透工具配置好密钥即可把端口暴露到公网上。这个公网就是填写配置类yml中notifyUrl的选项。

支付宝回调接口:

@Resource
private OrderMapper orderMapper;@PostMapping("/notify")  // 必须是POST接口
public String payNotify(HttpServletRequest request) throws Exception {if (request.getParameter("trade_status").equals("TRADE_SUCCESS")) {System.out.println("=========支付宝异步回调========");Map params = new HashMap<>();Map requestParams = request.getParameterMap();for (String name : requestParams.keySet()) {params.put(name, request.getParameter(name));}String tradeNo = params.get("out_trade_no");String payTime = params.get("gmt_payment");String alipayTradeNo = params.get("trade_no");// 支付宝验签if (Factory.Payment.Common().verifyNotify(params)) {// 验签通过System.out.println("交易名称: " + params.get("subject"));System.out.println("交易状态: " + params.get("trade_status"));System.out.println("支付宝交易凭证号: " + params.get("trade_no"));System.out.println("商户订单号: " + params.get("out_trade_no"));System.out.println("交易金额: " + params.get("total_amount"));System.out.println("买家在支付宝唯一id: " + params.get("buyer_id"));System.out.println("买家付款时间: " + params.get("gmt_payment"));System.out.println("买家付款金额: " + params.get("buyer_pay_amount"));// 更新订单未已支付[ORDER_PAID]orderMapper.updateState(Integer.valueOf(tradeNo), 1, payTime, alipayTradeNo);}}return "success";
}

支付成功后支付宝会调用本地此接口,这此接口支付宝会传进来很多参数,包含订单详情,支付宝流水号,支付款时间等等。

在此接口调用本地方法把订单状态改为已支付,即可完成此次支付操作。

9. 后台搭建

作为一个后台管理模块,需要一定的权限信息,只有给管理员方能访问后台接口,所以需要新建一张权限表。

image-20221222183605619

对应的把用户与对应的权限进行关联,所以再新建一张表:

image-20221222183620156

由于本项目使用了springSecurity框架,对于授权相对比较简单。

9.1 用户登录

9.1.1 需求分析

如下图若登录如何无管理员权限则提示错误。用户为系统管理员方可进入系统。

image-20221222185051162

后台系统不提供用户注册功能,只提供管理员登录功能。当前端表单发起请求过程中,后端security根据当前登录用户查询其权限,并封装为LoginUser

9.1.2 后端实现

UserDetailsService的实现类添加对应判断权限方法:

 @Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();// 方法引用queryWrapper.eq(StringUtils.isNotEmpty(username),User::getUsername,username);User user = userMapper.selectOne(queryWrapper);if (Objects.isNull(user)) {throw new UsernameNotFoundException("用户名或密码错误");}//判断用户是否被删除if (Objects.equals(user.getIsDelete(), IS_DELETED)) {throw new SystemException(AppHttpCodeEnum.USER_IS_DELETED);}log.info("数据库登录用户:{}",user);//TODO 查询角色权限List permissions = userMapper.selectRoleByUid(user.getUid());if (!permissions.contains(ROLE_ADMIN)){//非管理员throw new SystemException(AppHttpCodeEnum.NO_OPERATOR_AUTH);}log.info("当前登录用户:{};拥有权限:{}",user.getUsername(),permissions);return new LoginUser(user,permissions);
}

如果登录的用户不具有系统管理员ROLE_ADMIN权限,则返回前端403,信息为无权限操作。前端路由添加判断,若非管理员或未登录则页面必须跳转到登录接口。

// 权限验证不通过给出提示
if (res.code === 401 || res.code === 403) {ElementUI.Message({message: res.msg,type: 'error'});localStorage.removeItem("userInfo");localStorage.removeItem("_t");// window.location.reload();this.$router.replace("/login")
}

②如果登录的是管理员,则把权限封装进LoginUser,为了防止直接使用url访问会拿到数据。可以在配置中添加拦截器,添加鉴权拦截器。

LoginUser添加权限字段:

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {private User user;private List permissions;public LoginUser(User user, List permissions) {this.user = user;this.permissions = permissions;}/*** authorities 不会被序列化到 redis*/@JSONField(serialize = false)private List authorities;/*** 封装 permissions 权限信息** @return*/@Overridepublic Collection getAuthorities() {if (authorities != null) {return authorities;}authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

配置拦截器:

@Override
protected void configure(HttpSecurity http) throws Exception {http.csrf().disable();// 关闭csrfhttp//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口、注册接口 允许匿名访问.antMatchers("/user/login").anonymous()//注销接口需要认证才能访问.antMatchers("/user/logout").authenticated()// 配置权限.antMatchers("/user/*").hasAuthority("ROLE_ADMIN").antMatchers("/role/*").hasAuthority("ROLE_ADMIN").antMatchers("/goods/*").hasAuthority("ROLE_ADMIN")// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 配置异常处理器http.exceptionHandling()// 认证失败处理.authenticationEntryPoint(authenticationEntryPoint)// 授权失败.accessDeniedHandler(accessDeniedHandler);//关闭默认的注销功能http.logout().disable();//允许跨域http.cors();
}

登录接口的业务层方法:

登陆成功后,缓存LoginUser到redis。

/*** 登录** @param user* @return ResponseResult.okResult(userLoginVo)*/
@Override
public ResponseResult login(User user) {//判断用户名是否为空if (StringUtils.isEmpty(user.getUsername())) {throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);}UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());Authentication authentication = authenticationManager.authenticate(authenticationToken);//判断是否认证通过if (Objects.isNull(authentication)) {throw new RuntimeException("用户名或密码错误");}// 认证成功,从Authentication获取LoginUserLoginUser loginUser = (LoginUser) authentication.getPrincipal();log.info("loginUser:{}", loginUser);String userId = loginUser.getUser().getUid().toString();// 生成tokenString jwt = JwtUtil.createJWT(userId);// 存入redisredisCache.setCacheObject(LOGIN_ADMIN_KEY + userId, loginUser);UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);UserLoginVo userLoginVo = new UserLoginVo(jwt, userInfoVo);log.info("用户以登陆==>{}", userLoginVo);return ResponseResult.okResult(userLoginVo);
}

登陆成功来到用户管理:

image-20221222185556314

9.2 用户管理

9.2.1 需求分析

需要分页查询系统所有用户,并可进行模糊搜索。

然后就是添加用户、编辑用户、删除用户、批量删除用户功能。

9.2.2 后端实现

①以上全部接口需要管理员权限,前面配置类已配置过。接口一次性给出如下:

@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate UserService userService;@GetMapping("/username/{username}")public ResponseResult userInfo(@PathVariable String username) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUsername, username);return ResponseResult.okResult(userService.getOne(queryWrapper));}/*** 根据uid查询用户信息** @param uid* @return*/@GetMapping("/{uid}")public ResponseResult findOne(@PathVariable Integer uid) {return ResponseResult.okResult(userService.getUserInfo(uid));}/*** 新增或者更新** @param user* @return*/@PostMapping("/add")public ResponseResult addUser(@RequestBody User user) {return ResponseResult.okResult(userService.saveOrUpdateUser(user));}/*** 更新用户信息** @param user* @return*/@PostMapping("/update")public ResponseResult updateUser(@RequestBody User user) {return ResponseResult.okResult(userService.updateUserInfo(user));}@GetMapping("/page")public ResponseResult findPage(@RequestParam Integer pageNum,@RequestParam Integer pageSize,@RequestParam(defaultValue = "") String username,@RequestParam(defaultValue = "") String phone,@RequestParam(defaultValue = "") String email) {return ResponseResult.okResult(userService.userRolePage(pageNum,pageSize,username,phone,email));}/*** 把用户设置为删除状态** @param uid* @return*/@DeleteMapping("/{uid}")public ResponseResult setDeleted(@PathVariable Integer uid) {return ResponseResult.okResult(userService.setDeletedByUid(uid));}/*** 根据用户id删除** @param uid* @return*/@DeleteMapping("/del/{uid}")public ResponseResult deleteUser(@PathVariable Integer uid) {return ResponseResult.okResult(userService.removeById(uid));}/*** 批量删除** @param ids* @return*/@DeleteMapping("/del/batch")public ResponseResult deleteBatch(@RequestBody List ids) {return ResponseResult.okResult(userService.removeByIds(ids));}}

②业务层

@Slf4j
@Service("userService")
public class UserServiceImpl extends ServiceImpl implements UserService {@Resourceprivate RedisCache redisCache;@Resourceprivate UserMapper userMapper;@Overridepublic UserInfoVo getUserInfo(Integer uid) {//根据用户id查询用户信息LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ObjectUtils.isNotEmpty(uid), User::getUid, uid);User user = getOne(queryWrapper);//封装成UserInfoVoUserInfoVo vo = BeanCopyUtils.copyBean(user, UserInfoVo.class);return vo;}@Overridepublic boolean updateUserInfo(User user) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUid, user.getUid());return update(user, queryWrapper);}@Overridepublic Integer setDeletedByUid(Integer uid) {return userMapper.setDeletedByUid(uid);}@Resourceprivate RoleMapper roleMapper;@Overridepublic Page userRolePage(Integer pageNum, Integer pageSize, String username, String phone, String email) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.like(Strings.isNotEmpty(username), User::getUsername, username);queryWrapper.like(Strings.isNotEmpty(phone), User::getUsername, phone);queryWrapper.like(Strings.isNotEmpty(email), User::getUsername, email);Page userPage = page(new Page<>(pageNum, pageSize), queryWrapper);List userList = userPage.getRecords();List orderGoodVoList = new ArrayList<>();//封装用户权限userList.forEach(u -> {Integer uid = u.getUid();List userRoleList = roleMapper.getUserRoleList(uid);UserRoleVo userRoleVo = BeanCopyUtils.copyBean(u, UserRoleVo.class);userRoleVo.setRoleList(userRoleList);orderGoodVoList.add(userRoleVo);});Page userRoleVoPage = new Page<>();userRoleVoPage.setCurrent(userPage.getCurrent());userRoleVoPage.setPages(userPage.getPages());userRoleVoPage.setSize(userPage.getSize());userRoleVoPage.setTotal(userPage.getTotal());userRoleVoPage.setRecords(orderGoodVoList);return userRoleVoPage;}@Resourceprivate PasswordEncoder passwordEncoder;@Override@Transactionalpublic boolean saveOrUpdateUser(User user) {//密码加密String encode = passwordEncoder.encode(user.getPassword());user.setPassword(encode);userMapper.insertUser(user);System.out.println(user);Integer uid = user.getUid();log.info("新增加的用户==>{}",uid);//增加普通权限
//        userMapper.insertUserRole(uid,2);return true;}
}

9.3 商品管理

9.3.1 需求分析

image-20221222190537406

对数据库中商品表进行操作,包括基本CRUD。

9.3.2 后端实现

①控制层接口:

在控制层直接使用MP条件构造器进行操作简短省事整洁。

@RestController
@RequestMapping("/goods")
public class GoodsController {@Resourceprivate GoodsService goodsService;/*** 分页查询所有商品** @param pageNum* @param pageSize* @return*/@GetMapping("/page")public ResponseResult selectAll(@RequestParam Integer pageNum,@RequestParam Integer pageSize,@RequestParam(defaultValue = "") String title) {LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.like(Goods::getTitle,title);lambdaQueryWrapper.orderByDesc(Goods::getPriority);return ResponseResult.okResult(goodsService.page(new Page<>(pageNum, pageSize),lambdaQueryWrapper));}/*** 新增或者更新** @param goods* @return*/@PostMappingpublic ResponseResult addUser(@RequestBody Goods goods) {return ResponseResult.okResult(goodsService.saveOrUpdate(goods));}/*** 根据gid删除** @param gid* @return*/@DeleteMapping("/del/{gid}")public ResponseResult deleteUser(@PathVariable Integer gid) {return ResponseResult.okResult(goodsService.removeById(gid));}/*** 批量删除** @param ids* @return*/@DeleteMapping("/del/batch")public ResponseResult deleteBatch(@RequestBody List ids) {return ResponseResult.okResult(goodsService.removeByIds(ids));}
}

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...