Compare commits
2 Commits
bd50b17709
...
6d01440d3f
Author | SHA1 | Date |
---|---|---|
土豆兄弟 | 6d01440d3f | 3 weeks ago |
土豆兄弟 | 83a5b921aa | 4 weeks ago |
@ -0,0 +1,29 @@
|
|||||||
|
## SpringCloud Netflix Hystrix
|
||||||
|
|
||||||
|
|
||||||
|
### SpringCloud Netflix Hystrix 概览
|
||||||
|
|
||||||
|
|
||||||
|
### 使用注解方式实现服务的容错、降级
|
||||||
|
|
||||||
|
|
||||||
|
### 使用编程方式实现服务的容错、降级
|
||||||
|
|
||||||
|
|
||||||
|
### 编程方式开启 Hystrix 请求缓存
|
||||||
|
|
||||||
|
|
||||||
|
### 注解方式开启 Hystrix 请求缓存
|
||||||
|
|
||||||
|
|
||||||
|
### 编程方式应用 Hystrix 请求合并
|
||||||
|
|
||||||
|
|
||||||
|
### 注解方式应用 Hystrix 请求合并
|
||||||
|
|
||||||
|
### OpenFeign 集成 Hystrix 开启后备模式
|
||||||
|
|
||||||
|
|
||||||
|
### 使用 Hystrix 监控面板监测客户端容错
|
||||||
|
|
||||||
|
### SpringCloud Netflix Hystrix 容错组件总结
|
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
@ -0,0 +1,20 @@
|
|||||||
|
package org.example;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户账户微服务启动入口</h1>
|
||||||
|
* 127.0.0.1:8003/dev-protocol-springcloud-project-account-service/swagger-ui.html 原生的地址信息
|
||||||
|
* 127.0.0.1:8003/dev-protocol-springcloud-project-account-service/doc.html 美化之后的地址信息
|
||||||
|
* */
|
||||||
|
@EnableJpaAuditing
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableDiscoveryClient
|
||||||
|
public class AccountApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(AccountApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package org.example.controller;
|
||||||
|
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.example.account.AddressInfo;
|
||||||
|
import org.example.common.TableId;
|
||||||
|
import org.example.service.IAddressService;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户地址服务 Controller</h1>
|
||||||
|
* */
|
||||||
|
@Api(tags = "用户地址服务")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/address")
|
||||||
|
public class AddressController {
|
||||||
|
|
||||||
|
private final IAddressService addressService;
|
||||||
|
|
||||||
|
public AddressController(IAddressService addressService) {
|
||||||
|
this.addressService = addressService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// value 是简述, notes 是详细的描述信息
|
||||||
|
@ApiOperation(value = "创建", notes = "创建用户地址信息", httpMethod = "POST")
|
||||||
|
@PostMapping("/create-address")
|
||||||
|
public TableId createAddressInfo(@RequestBody AddressInfo addressInfo) {
|
||||||
|
return addressService.createAddressInfo(addressInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "当前用户", notes = "获取当前登录用户地址信息", httpMethod = "GET")
|
||||||
|
@GetMapping("/current-address")
|
||||||
|
public AddressInfo getCurrentAddressInfo() {
|
||||||
|
return addressService.getCurrentAddressInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "获取用户地址信息",
|
||||||
|
notes = "通过 id 获取用户地址信息, id 是 Address 表的主键",
|
||||||
|
httpMethod = "GET")
|
||||||
|
@GetMapping("/address-info")
|
||||||
|
public AddressInfo getAddressInfoById(@RequestParam Long id) {
|
||||||
|
return addressService.getAddressInfoById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "获取用户地址信息",
|
||||||
|
notes = "通过 TableId 获取用户地址信息", httpMethod = "POST")
|
||||||
|
@PostMapping("/address-info-by-table-id")
|
||||||
|
public AddressInfo getAddressInfoByTablesId(@RequestBody TableId tableId) {
|
||||||
|
return addressService.getAddressInfoByTableId(tableId);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package org.example.controller;
|
||||||
|
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.example.account.BalanceInfo;
|
||||||
|
import org.example.service.IBalanceService;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户余额服务 Controller</h1>
|
||||||
|
* */
|
||||||
|
@Api(tags = "用户余额服务")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/balance")
|
||||||
|
public class BalanceController {
|
||||||
|
|
||||||
|
private final IBalanceService balanceService;
|
||||||
|
|
||||||
|
public BalanceController(IBalanceService balanceService) {
|
||||||
|
this.balanceService = balanceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "当前用户", notes = "获取当前用户余额信息", httpMethod = "GET")
|
||||||
|
@GetMapping("/current-balance")
|
||||||
|
public BalanceInfo getCurrentUserBalanceInfo() {
|
||||||
|
return balanceService.getCurrentUserBalanceInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "扣减", notes = "扣减用于余额", httpMethod = "PUT")
|
||||||
|
@PutMapping("/deduct-balance")
|
||||||
|
public BalanceInfo deductBalance(@RequestBody BalanceInfo balanceInfo) {
|
||||||
|
return balanceService.deductBalance(balanceInfo);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package org.example.dao;
|
||||||
|
|
||||||
|
import org.example.entity.Address;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>EcommerceAddress Dao 接口定义</h1>
|
||||||
|
* */
|
||||||
|
public interface EcommerceAddressDao extends JpaRepository<Address, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>根据 用户 id 查询地址信息</h2>
|
||||||
|
* */
|
||||||
|
List<Address> findAllByUserId(Long userId);
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package org.example.dao;
|
||||||
|
|
||||||
|
import org.example.entity.Balance;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>Balance Dao 接口定义</h1>
|
||||||
|
* */
|
||||||
|
public interface EcommerceBalanceDao extends JpaRepository<Balance, Long> {
|
||||||
|
|
||||||
|
/** 根据 userId 查询 EcommerceBalance 对象 */
|
||||||
|
Balance findByUserId(Long userId);
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
package org.example.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.example.account.AddressInfo;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EntityListeners;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户地址表实体类定义</h1>
|
||||||
|
* */
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Entity
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
@Table(name = "t_dev_protocol_cloud_address")
|
||||||
|
public class Address {
|
||||||
|
|
||||||
|
/** 自增主键 */
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id", nullable = false)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 用户 id */
|
||||||
|
@Column(name = "user_id", nullable = false)
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
@Column(name = "username", nullable = false)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/** 电话 */
|
||||||
|
@Column(name = "phone", nullable = false)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/** 省 */
|
||||||
|
@Column(name = "province", nullable = false)
|
||||||
|
private String province;
|
||||||
|
|
||||||
|
/** 市 */
|
||||||
|
@Column(name = "city", nullable = false)
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
/** 详细地址 */
|
||||||
|
@Column(name = "address_detail", nullable = false)
|
||||||
|
private String addressDetail;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "create_time", nullable = false)
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "update_time", nullable = false)
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>根据 userId + AddressItem 得到 Address</h2>
|
||||||
|
* */
|
||||||
|
public static Address to(Long userId, AddressInfo.AddressItem addressItem) {
|
||||||
|
|
||||||
|
Address ecommerceAddress = new Address();
|
||||||
|
|
||||||
|
ecommerceAddress.setUserId(userId);
|
||||||
|
ecommerceAddress.setUsername(addressItem.getUsername());
|
||||||
|
ecommerceAddress.setPhone(addressItem.getPhone());
|
||||||
|
ecommerceAddress.setProvince(addressItem.getProvince());
|
||||||
|
ecommerceAddress.setCity(addressItem.getCity());
|
||||||
|
ecommerceAddress.setAddressDetail(addressItem.getAddressDetail());
|
||||||
|
|
||||||
|
return ecommerceAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>将 Address 对象转成 AddressInfo</h2>
|
||||||
|
* */
|
||||||
|
public AddressInfo.AddressItem toAddressItem() {
|
||||||
|
|
||||||
|
AddressInfo.AddressItem addressItem = new AddressInfo.AddressItem();
|
||||||
|
|
||||||
|
addressItem.setId(this.id);
|
||||||
|
addressItem.setUsername(this.username);
|
||||||
|
addressItem.setPhone(this.phone);
|
||||||
|
addressItem.setProvince(this.province);
|
||||||
|
addressItem.setCity(this.city);
|
||||||
|
addressItem.setAddressDetail(this.addressDetail);
|
||||||
|
addressItem.setCreateTime(this.createTime);
|
||||||
|
addressItem.setUpdateTime(this.updateTime);
|
||||||
|
|
||||||
|
return addressItem;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package org.example.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EntityListeners;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户账户余额表实体类定义</h1>
|
||||||
|
* */
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Entity
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
@Table(name = "t_dev_protocol_cloud_balance")
|
||||||
|
public class Balance {
|
||||||
|
|
||||||
|
/** 自增主键 */
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id", nullable = false)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 用户 id */
|
||||||
|
@Column(name = "user_id", nullable = false)
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 账户余额 */
|
||||||
|
@Column(name = "balance", nullable = false)
|
||||||
|
private Long balance;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "create_time", nullable = false)
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "update_time", nullable = false)
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.example.service;
|
||||||
|
|
||||||
|
|
||||||
|
import org.example.account.AddressInfo;
|
||||||
|
import org.example.common.TableId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户地址相关服务接口定义</h1>
|
||||||
|
* */
|
||||||
|
public interface IAddressService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>创建用户地址信息</h2>
|
||||||
|
* */
|
||||||
|
TableId createAddressInfo(AddressInfo addressInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>获取当前登录的用户地址信息</h2>
|
||||||
|
* */
|
||||||
|
AddressInfo getCurrentAddressInfo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>通过 id 获取用户地址信息, id 是 Address 表的主键</h2>
|
||||||
|
* */
|
||||||
|
AddressInfo getAddressInfoById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>通过 TableId 获取用户地址信息</h2>
|
||||||
|
* */
|
||||||
|
AddressInfo getAddressInfoByTableId(TableId tableId);
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.example.service;
|
||||||
|
|
||||||
|
|
||||||
|
import org.example.account.BalanceInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>用于余额相关的服务接口定义</h2>
|
||||||
|
* */
|
||||||
|
public interface IBalanceService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>获取当前用户余额信息</h2>
|
||||||
|
* */
|
||||||
|
BalanceInfo getCurrentUserBalanceInfo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>扣减用户余额</h2>
|
||||||
|
* @param balanceInfo 代表想要扣减的余额
|
||||||
|
* */
|
||||||
|
BalanceInfo deductBalance(BalanceInfo balanceInfo);
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
package org.example.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.example.account.AddressInfo;
|
||||||
|
import org.example.common.TableId;
|
||||||
|
import org.example.dao.EcommerceAddressDao;
|
||||||
|
import org.example.entity.Address;
|
||||||
|
import org.example.filter.AccessContext;
|
||||||
|
import org.example.service.IAddressService;
|
||||||
|
import org.example.vo.LoginUserInfo;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户地址相关服务接口实现</h1>
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public class AddressServiceImpl implements IAddressService {
|
||||||
|
|
||||||
|
private final EcommerceAddressDao addressDao;
|
||||||
|
|
||||||
|
public AddressServiceImpl(EcommerceAddressDao addressDao) {
|
||||||
|
this.addressDao = addressDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>存储多个地址信息</h2>
|
||||||
|
* */
|
||||||
|
@Override
|
||||||
|
public TableId createAddressInfo(AddressInfo addressInfo) {
|
||||||
|
|
||||||
|
// 不能直接从参数中获取用户的 id 信息
|
||||||
|
LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();
|
||||||
|
|
||||||
|
// 将传递的参数转换成实体对象
|
||||||
|
List<Address> ecommerceAddresses = addressInfo.getAddressItems().stream()
|
||||||
|
.map(a -> Address.to(loginUserInfo.getId(), a))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 保存到数据表并把返回记录的 id 给调用方
|
||||||
|
List<Address> savedRecords = addressDao.saveAll(ecommerceAddresses);
|
||||||
|
List<Long> ids = savedRecords.stream()
|
||||||
|
.map(Address::getId).collect(Collectors.toList());
|
||||||
|
log.info("create address info: [{}], [{}]", loginUserInfo.getId(),
|
||||||
|
JSON.toJSONString(ids));
|
||||||
|
|
||||||
|
return new TableId(
|
||||||
|
ids.stream().map(TableId.Id::new).collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AddressInfo getCurrentAddressInfo() {
|
||||||
|
|
||||||
|
LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();
|
||||||
|
|
||||||
|
// 根据 userId 查询到用户的地址信息, 再实现转换
|
||||||
|
List<Address> ecommerceAddresses = addressDao.findAllByUserId(
|
||||||
|
loginUserInfo.getId()
|
||||||
|
);
|
||||||
|
List<AddressInfo.AddressItem> addressItems = ecommerceAddresses.stream()
|
||||||
|
.map(Address::toAddressItem)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new AddressInfo(loginUserInfo.getId(), addressItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AddressInfo getAddressInfoById(Long id) {
|
||||||
|
|
||||||
|
Address ecommerceAddress = addressDao.findById(id).orElse(null);
|
||||||
|
if (null == ecommerceAddress) {
|
||||||
|
throw new RuntimeException("address is not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AddressInfo(
|
||||||
|
ecommerceAddress.getUserId(),
|
||||||
|
Collections.singletonList(ecommerceAddress.toAddressItem())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AddressInfo getAddressInfoByTableId(TableId tableId) {
|
||||||
|
|
||||||
|
List<Long> ids = tableId.getIds().stream()
|
||||||
|
.map(TableId.Id::getId).collect(Collectors.toList());
|
||||||
|
log.info("get address info by table id: [{}]", JSON.toJSONString(ids));
|
||||||
|
|
||||||
|
List<Address> ecommerceAddresses = addressDao.findAllById(ids);
|
||||||
|
if (CollectionUtils.isEmpty(ecommerceAddresses)) {
|
||||||
|
return new AddressInfo(-1L, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AddressInfo.AddressItem> addressItems = ecommerceAddresses.stream()
|
||||||
|
.map(Address::toAddressItem)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new AddressInfo(
|
||||||
|
ecommerceAddresses.get(0).getUserId(), addressItems
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
server:
|
||||||
|
port: 8003
|
||||||
|
servlet:
|
||||||
|
context-path: /dev-protocol-springcloud-project-account-service
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: dev-protocol-springcloud-project-account-service
|
||||||
|
cloud:
|
||||||
|
nacos:
|
||||||
|
discovery:
|
||||||
|
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
|
||||||
|
server-addr: 127.0.0.1:8848
|
||||||
|
# server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
|
||||||
|
namespace: 1ccc74ae-9398-4dbe-b9d7-4f9addf9f40c
|
||||||
|
metadata:
|
||||||
|
management:
|
||||||
|
context-path: ${server.servlet.context-path}/actuator
|
||||||
|
kafka:
|
||||||
|
bootstrap-servers: 127.0.0.1:9092
|
||||||
|
producer:
|
||||||
|
retries: 3
|
||||||
|
consumer:
|
||||||
|
auto-offset-reset: latest
|
||||||
|
sleuth:
|
||||||
|
sampler:
|
||||||
|
probability: 1.0 # 采样比例, 1.0 表示 100%, 默认是 0.1
|
||||||
|
zipkin:
|
||||||
|
sender:
|
||||||
|
type: kafka # 默认是 web
|
||||||
|
base-url: http://localhost:9411/
|
||||||
|
jpa:
|
||||||
|
show-sql: true
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: none
|
||||||
|
properties:
|
||||||
|
hibernate.show_sql: true
|
||||||
|
hibernate.format_sql: true
|
||||||
|
hibernate:
|
||||||
|
dialect: org.hibernate.dialect.MySQLDialect
|
||||||
|
open-in-view: false
|
||||||
|
datasource:
|
||||||
|
# 数据源
|
||||||
|
url: jdbc:mysql://127.0.0.1:3306/dev_protocol_springcloud_project?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
||||||
|
username: root
|
||||||
|
password: root
|
||||||
|
type: com.zaxxer.hikari.HikariDataSource
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
# 连接池
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 8
|
||||||
|
minimum-idle: 4
|
||||||
|
idle-timeout: 30000
|
||||||
|
connection-timeout: 30000
|
||||||
|
max-lifetime: 45000
|
||||||
|
auto-commit: true
|
||||||
|
pool-name: devProtocolSpringcloudHikariCP
|
||||||
|
|
||||||
|
# 暴露端点
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: '*'
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
@ -0,0 +1,43 @@
|
|||||||
|
### 创建用户地址信息
|
||||||
|
POST http://127.0.0.1:9001/dev-protocol-springcloud-gateway/dev-protocol-springcloud-project-account-service/address/create-address
|
||||||
|
Content-Type: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjU2ZGViY2NiLTlkNTEtNDMzMC04NjFhLTY5MWI0YWE2NzY1MiIsImV4cCI6MTYyNDcyMzIwMH0.M9Xg__zvXvAeALMcAs1LIJN41_JGo7Od1bw0218AbmzrBMZ-9WT6plow62gSBfxa5Xbm79t6VeHq8iC7a0ZYBF2RemNaXmrgIkmwxjg12jO56hTCRv1jghAbiFZknTxXA4QQ6dB3MS4mE19MDt6P3f3ckLwyl-IjE9O5_c-wg47Sb8odfrm_K2RMx_2ZMQAfNjHzxCzrQyNRWhqSQTpSVAuUScaxlnf4EvEwav8FT0QICIlx8oZyU7fh_zBtfFL4EhQlHfcU-eo6Nw4YUVA0knIrkAXD8jqCowx7Mej8khQ6zXNUB2V_uTtc2P2VW1UHlWSAiFCR_qkvBGSWO0VH_w
|
||||||
|
|
||||||
|
{
|
||||||
|
"userId": 10,
|
||||||
|
"addressItems": [
|
||||||
|
{
|
||||||
|
"username": "zxcx",
|
||||||
|
"phone": "16600000001",
|
||||||
|
"province": "上海市",
|
||||||
|
"city": "上海市",
|
||||||
|
"addressDetail": "闵行区"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
### 当前登录用户地址信息
|
||||||
|
GET http://127.0.0.1:9001/dev-protocol-springcloud-gateway/dev-protocol-springcloud-project-account-service/address/current-address
|
||||||
|
Accept: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjU2ZGViY2NiLTlkNTEtNDMzMC04NjFhLTY5MWI0YWE2NzY1MiIsImV4cCI6MTYyNDcyMzIwMH0.M9Xg__zvXvAeALMcAs1LIJN41_JGo7Od1bw0218AbmzrBMZ-9WT6plow62gSBfxa5Xbm79t6VeHq8iC7a0ZYBF2RemNaXmrgIkmwxjg12jO56hTCRv1jghAbiFZknTxXA4QQ6dB3MS4mE19MDt6P3f3ckLwyl-IjE9O5_c-wg47Sb8odfrm_K2RMx_2ZMQAfNjHzxCzrQyNRWhqSQTpSVAuUScaxlnf4EvEwav8FT0QICIlx8oZyU7fh_zBtfFL4EhQlHfcU-eo6Nw4YUVA0knIrkAXD8jqCowx7Mej8khQ6zXNUB2V_uTtc2P2VW1UHlWSAiFCR_qkvBGSWO0VH_w
|
||||||
|
|
||||||
|
### 通过 id 获取用户地址信息
|
||||||
|
GET http://127.0.0.1:9001/dev-protocol-springcloud-gateway/dev-protocol-springcloud-project-account-service/address/address-info?id=2
|
||||||
|
Accept: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjU2ZGViY2NiLTlkNTEtNDMzMC04NjFhLTY5MWI0YWE2NzY1MiIsImV4cCI6MTYyNDcyMzIwMH0.M9Xg__zvXvAeALMcAs1LIJN41_JGo7Od1bw0218AbmzrBMZ-9WT6plow62gSBfxa5Xbm79t6VeHq8iC7a0ZYBF2RemNaXmrgIkmwxjg12jO56hTCRv1jghAbiFZknTxXA4QQ6dB3MS4mE19MDt6P3f3ckLwyl-IjE9O5_c-wg47Sb8odfrm_K2RMx_2ZMQAfNjHzxCzrQyNRWhqSQTpSVAuUScaxlnf4EvEwav8FT0QICIlx8oZyU7fh_zBtfFL4EhQlHfcU-eo6Nw4YUVA0knIrkAXD8jqCowx7Mej8khQ6zXNUB2V_uTtc2P2VW1UHlWSAiFCR_qkvBGSWO0VH_w
|
||||||
|
|
||||||
|
### 获取用户地址信息
|
||||||
|
POST http://127.0.0.1:9001/dev-protocol-springcloud-gateway/dev-protocol-springcloud-project-account-service/address/address-info-by-table-id
|
||||||
|
Content-Type: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjU2ZGViY2NiLTlkNTEtNDMzMC04NjFhLTY5MWI0YWE2NzY1MiIsImV4cCI6MTYyNDcyMzIwMH0.M9Xg__zvXvAeALMcAs1LIJN41_JGo7Od1bw0218AbmzrBMZ-9WT6plow62gSBfxa5Xbm79t6VeHq8iC7a0ZYBF2RemNaXmrgIkmwxjg12jO56hTCRv1jghAbiFZknTxXA4QQ6dB3MS4mE19MDt6P3f3ckLwyl-IjE9O5_c-wg47Sb8odfrm_K2RMx_2ZMQAfNjHzxCzrQyNRWhqSQTpSVAuUScaxlnf4EvEwav8FT0QICIlx8oZyU7fh_zBtfFL4EhQlHfcU-eo6Nw4YUVA0knIrkAXD8jqCowx7Mej8khQ6zXNUB2V_uTtc2P2VW1UHlWSAiFCR_qkvBGSWO0VH_w
|
||||||
|
|
||||||
|
{
|
||||||
|
"ids": [
|
||||||
|
{
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
### 获取当前用户余额信息
|
||||||
|
GET http://127.0.0.1:9001/dev-protocol-springcloud-gateway/dev-protocol-springcloud-project-account-service/balance/current-balance
|
||||||
|
Accept: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjU2ZGViY2NiLTlkNTEtNDMzMC04NjFhLTY5MWI0YWE2NzY1MiIsImV4cCI6MTYyNDcyMzIwMH0.M9Xg__zvXvAeALMcAs1LIJN41_JGo7Od1bw0218AbmzrBMZ-9WT6plow62gSBfxa5Xbm79t6VeHq8iC7a0ZYBF2RemNaXmrgIkmwxjg12jO56hTCRv1jghAbiFZknTxXA4QQ6dB3MS4mE19MDt6P3f3ckLwyl-IjE9O5_c-wg47Sb8odfrm_K2RMx_2ZMQAfNjHzxCzrQyNRWhqSQTpSVAuUScaxlnf4EvEwav8FT0QICIlx8oZyU7fh_zBtfFL4EhQlHfcU-eo6Nw4YUVA0knIrkAXD8jqCowx7Mej8khQ6zXNUB2V_uTtc2P2VW1UHlWSAiFCR_qkvBGSWO0VH_w
|
||||||
|
|
||||||
|
### 扣减用户余额
|
||||||
|
PUT http://127.0.0.1:9001/dev-protocol-springcloud-gateway/dev-protocol-springcloud-project-account-service/balance/deduct-balance
|
||||||
|
Content-Type: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjU2ZGViY2NiLTlkNTEtNDMzMC04NjFhLTY5MWI0YWE2NzY1MiIsImV4cCI6MTYyNDcyMzIwMH0.M9Xg__zvXvAeALMcAs1LIJN41_JGo7Od1bw0218AbmzrBMZ-9WT6plow62gSBfxa5Xbm79t6VeHq8iC7a0ZYBF2RemNaXmrgIkmwxjg12jO56hTCRv1jghAbiFZknTxXA4QQ6dB3MS4mE19MDt6P3f3ckLwyl-IjE9O5_c-wg47Sb8odfrm_K2RMx_2ZMQAfNjHzxCzrQyNRWhqSQTpSVAuUScaxlnf4EvEwav8FT0QICIlx8oZyU7fh_zBtfFL4EhQlHfcU-eo6Nw4YUVA0knIrkAXD8jqCowx7Mej8khQ6zXNUB2V_uTtc2P2VW1UHlWSAiFCR_qkvBGSWO0VH_w
|
||||||
|
|
||||||
|
{
|
||||||
|
"userId": 10,
|
||||||
|
"balance": 2000
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
-- 创建 t_ecommerce_address 数据表
|
||||||
|
CREATE TABLE IF NOT EXISTS `dev_protocol_springcloud_project`.`t_dev_protocol_cloud_address` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
|
||||||
|
`user_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '用户 id',
|
||||||
|
`username` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名',
|
||||||
|
`phone` varchar(64) NOT NULL DEFAULT '' COMMENT '电话号码',
|
||||||
|
`province` varchar(64) NOT NULL DEFAULT '' COMMENT '省',
|
||||||
|
`city` varchar(64) NOT NULL DEFAULT '' COMMENT '市',
|
||||||
|
`address_detail` varchar(256) NOT NULL DEFAULT '' COMMENT '详细地址',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户地址表';
|
@ -0,0 +1,10 @@
|
|||||||
|
-- 创建 t_ecommerce_balance 数据表
|
||||||
|
CREATE TABLE IF NOT EXISTS `dev_protocol_springcloud_project`.`t_dev_protocol_cloud_balance` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
|
||||||
|
`user_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '用户 id',
|
||||||
|
`balance` bigint(20) NOT NULL DEFAULT 0 COMMENT '账户余额',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `user_id_key` (`user_id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户账户余额表';
|
@ -0,0 +1,19 @@
|
|||||||
|
package org.example;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户账户微服务环境可用性校验</h1>
|
||||||
|
* */
|
||||||
|
@SpringBootTest
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
public class AccountApplicationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contextLoad() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package org.example.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.example.account.AddressInfo;
|
||||||
|
import org.example.common.TableId;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户地址相关服务功能测试</h1>
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
public class AddressServiceTest extends BaseTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAddressService addressService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>测试创建用户地址信息</h2>
|
||||||
|
* */
|
||||||
|
@Test
|
||||||
|
public void testCreateAddressInfo() {
|
||||||
|
|
||||||
|
AddressInfo.AddressItem addressItem = new AddressInfo.AddressItem();
|
||||||
|
addressItem.setUsername("qqq");
|
||||||
|
addressItem.setPhone("18800000001");
|
||||||
|
addressItem.setProvince("上海市");
|
||||||
|
addressItem.setCity("上海市");
|
||||||
|
addressItem.setAddressDetail("陆家嘴");
|
||||||
|
|
||||||
|
log.info("test create address info: [{}]", JSON.toJSONString(
|
||||||
|
addressService.createAddressInfo(
|
||||||
|
new AddressInfo(loginUserInfo.getId(),
|
||||||
|
Collections.singletonList(addressItem))
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>测试获取当前登录用户地址信息</h2>
|
||||||
|
* */
|
||||||
|
@Test
|
||||||
|
public void testGetCurrentAddressInfo() {
|
||||||
|
|
||||||
|
log.info("test get current user info: [{}]", JSON.toJSONString(
|
||||||
|
addressService.getCurrentAddressInfo()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>测试通过 id 获取用户地址信息</h2>
|
||||||
|
* */
|
||||||
|
@Test
|
||||||
|
public void testGetAddressInfoById() {
|
||||||
|
|
||||||
|
log.info("test get address info by id: [{}]", JSON.toJSONString(
|
||||||
|
addressService.getAddressInfoById(1L)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>测试通过 TableId 获取用户地址信息</h2>
|
||||||
|
* */
|
||||||
|
@Test
|
||||||
|
public void testGetAddressInfoByTableId() {
|
||||||
|
|
||||||
|
log.info("test get address info by table id: [{}]", JSON.toJSONString(
|
||||||
|
addressService.getAddressInfoByTableId(
|
||||||
|
new TableId(Collections.singletonList(new TableId.Id(1L)))
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.example.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.example.account.BalanceInfo;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用于余额相关服务测试</h1>
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
public class BalanceServiceTest extends BaseTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IBalanceService balanceService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>测试获取当前用户的余额信息</h2>
|
||||||
|
* */
|
||||||
|
@Test
|
||||||
|
public void testGetCurrentUserBalanceInfo() {
|
||||||
|
|
||||||
|
log.info("test get current user balance info: [{}]", JSON.toJSONString(
|
||||||
|
balanceService.getCurrentUserBalanceInfo()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>测试扣减用于余额</h2>
|
||||||
|
* */
|
||||||
|
@Test
|
||||||
|
public void testDeductBalance() {
|
||||||
|
|
||||||
|
BalanceInfo balanceInfo = new BalanceInfo();
|
||||||
|
balanceInfo.setUserId(loginUserInfo.getId());
|
||||||
|
balanceInfo.setBalance(1000L);
|
||||||
|
|
||||||
|
log.info("test deduct balance: [{}]", JSON.toJSONString(
|
||||||
|
balanceService.deductBalance(balanceInfo)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.example.service;
|
||||||
|
|
||||||
|
import org.example.filter.AccessContext;
|
||||||
|
import org.example.vo.LoginUserInfo;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>测试用例基类, 填充登录用户信息</h1>
|
||||||
|
* */
|
||||||
|
@SpringBootTest
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
public abstract class BaseTest {
|
||||||
|
|
||||||
|
protected final LoginUserInfo loginUserInfo = new LoginUserInfo(
|
||||||
|
10L, "q@bbbbbbyyyyyy.com"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void init() {
|
||||||
|
AccessContext.setLoginUserInfo(loginUserInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void destroy() {
|
||||||
|
AccessContext.clearLoginUserInfo();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
## 用户账户微服务
|
||||||
|
|
||||||
|
### 用户身份登录统一拦截
|
||||||
|
- 所有需要开发的微服务通用的配置
|
||||||
|
|
||||||
|
### 集成 Swagger2 实现代码即文档
|
||||||
|
-
|
||||||
|
|
||||||
|
### 用户账户微服务功能设计
|
||||||
|
-
|
||||||
|
|
||||||
|
### 数据表及 ORM 过程
|
||||||
|
-
|
||||||
|
|
||||||
|
### 用户地址与余额服务接口定义
|
||||||
|
- Service 返回包装成类进行返回, 尽量不要仅仅只返回一个ID, 防止后续的扩展需求
|
||||||
|
- 不要设计通过请求参数来获取当前用户的余额信息, 因为可以通过请求劫持来拿到别的请求信息, 防止越权
|
||||||
|
### 用户地址相关服务接口实现
|
||||||
|
|
||||||
|
|
||||||
|
### 用户地址服务接口可用性测试(测试用例)
|
||||||
|
|
||||||
|
|
||||||
|
### 用户余额相关服务接口实现
|
||||||
|
|
||||||
|
|
||||||
|
### 用户余额服务接口可用性测试(测试用例)
|
||||||
|
|
||||||
|
|
||||||
|
### 用户账户微服务对外 HTTP 接口
|
||||||
|
|
||||||
|
|
||||||
|
### 验证用户账户微服务功能可用性
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "dev-protocol-springcloud-project-account-service",
|
||||||
|
"order": 0, // order 越小优先级越高0
|
||||||
|
"predicates": [
|
||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"pattern": "/dev-protocol-springcloud-gateway/dev-protocol-springcloud-project-account-service/**"
|
||||||
|
},
|
||||||
|
"name": "Path"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uri": "lb://dev-protocol-springcloud-project-account-service",
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"name": "HeaderToken"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "StripPrefix",
|
||||||
|
"args": {
|
||||||
|
"parts": "1" // 用来跳过上面的 pattern 前面的配置, 因为 [bootstrap.yml] 中的配置 context-path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
- 补充放在nacos 的 gateway 的配置文件中
|
||||||
|
### 用户账户微服务总结
|
||||||
|
|
||||||
|
- 微服务开始之前的准备工作
|
||||||
|
- 用户身份登录拦截
|
||||||
|
- 在请求进入 service 之前解析 header 中的 token 信息, 并填充用户信息到上下文中
|
||||||
|
- 在请求结束之后,清理掉上下文中的用户信息
|
||||||
|
- 对于一些特定的 HTTP 请求不要拦截(即白名单 )
|
||||||
|
- 代码即文档:引入 Swagger
|
||||||
|
- pom 中添加依赖配置
|
||||||
|
- 自定义配置 Swagger
|
||||||
|
---
|
||||||
|
- 微服务模块的设计思想
|
||||||
|
- 微服务模块应该是低耦合、尽可能多的重用代码
|
||||||
|
- Tips:设计并不唯-!
|
||||||
|
- ![用户账户微服务总设计.png](pic/用户账户微服务总设计.png)
|
||||||
|
---
|
||||||
|
- 用户账户微服务的功能及在业务中的位置
|
||||||
|
- ![账户微服务在业务中的位置.png](pic/账户微服务在业务中的位置.png)
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.example;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品微服务启动入口</h1>
|
||||||
|
* 启动依赖组件/中间件: Redis + MySQL + Nacos + Kafka + Zipkin
|
||||||
|
* http://127.0.0.1:8001/dev-protocol-springcloud-project-goods-service/doc.html
|
||||||
|
* */
|
||||||
|
@EnableJpaAuditing
|
||||||
|
@EnableDiscoveryClient
|
||||||
|
@SpringBootApplication
|
||||||
|
public class GoodsApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
SpringApplication.run(GoodsApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package org.example.config;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>自定义异步任务线程池, 异步任务异常捕获处理器</h1>
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
@EnableAsync // 开启 Spring 异步任务支持
|
||||||
|
@Configuration
|
||||||
|
public class AsyncPoolConfig implements AsyncConfigurer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>将自定义的线程池注入到 Spring 容器中</h2>
|
||||||
|
* */
|
||||||
|
@Bean
|
||||||
|
@Override
|
||||||
|
public Executor getAsyncExecutor() {
|
||||||
|
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
|
||||||
|
executor.setCorePoolSize(10);
|
||||||
|
executor.setMaxPoolSize(20);
|
||||||
|
executor.setQueueCapacity(20);
|
||||||
|
executor.setKeepAliveSeconds(60);
|
||||||
|
executor.setThreadNamePrefix("Q-Async-"); // 这个非常重要
|
||||||
|
|
||||||
|
// 等待所有任务结果候再关闭线程池
|
||||||
|
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
executor.setAwaitTerminationSeconds(60);
|
||||||
|
// 定义拒绝策略
|
||||||
|
executor.setRejectedExecutionHandler(
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy()
|
||||||
|
);
|
||||||
|
// 初始化线程池, 初始化 core 线程
|
||||||
|
executor.initialize();
|
||||||
|
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>指定系统中的异步任务在出现异常时使用到的处理器</h2>
|
||||||
|
* */
|
||||||
|
@Override
|
||||||
|
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
|
||||||
|
return new AsyncExceptionHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>异步任务异常捕获处理器</h2>
|
||||||
|
* */
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleUncaughtException(Throwable throwable, Method method,
|
||||||
|
Object... objects) {
|
||||||
|
|
||||||
|
throwable.printStackTrace();
|
||||||
|
log.error("Async Error: [{}], Method: [{}], Param: [{}]",
|
||||||
|
throwable.getMessage(), method.getName(),
|
||||||
|
JSON.toJSONString(objects));
|
||||||
|
|
||||||
|
// TODO 发送邮件或者是短信, 做进一步的报警处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package org.example.constant;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>异步任务状态枚举</h1>
|
||||||
|
* */
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum AsyncTaskStatusEnum {
|
||||||
|
|
||||||
|
STARTED(0, "已经启动"),
|
||||||
|
RUNNING(1, "正在运行"),
|
||||||
|
SUCCESS(2, "执行成功"),
|
||||||
|
FAILED(3, "执行失败"),
|
||||||
|
;
|
||||||
|
|
||||||
|
/** 执行状态编码 */
|
||||||
|
private final int state;
|
||||||
|
|
||||||
|
/** 执行状态描述 */
|
||||||
|
private final String stateInfo;
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.example.constant;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>品牌分类</h1>
|
||||||
|
* */
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum BrandCategory {
|
||||||
|
|
||||||
|
BRAND_A("20001", "品牌A"),
|
||||||
|
BRAND_B("20002", "品牌B"),
|
||||||
|
BRAND_C("20003", "品牌C"),
|
||||||
|
BRAND_D("20004", "品牌D"),
|
||||||
|
BRAND_E("20005", "品牌E"),
|
||||||
|
;
|
||||||
|
|
||||||
|
/** 品牌分类编码 */
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
/** 品牌分类描述信息 */
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>根据 code 获取到 BrandCategory</h2>
|
||||||
|
* */
|
||||||
|
public static BrandCategory of(String code) {
|
||||||
|
|
||||||
|
Objects.requireNonNull(code);
|
||||||
|
|
||||||
|
return Stream.of(values())
|
||||||
|
.filter(bean -> bean.code.equals(code))
|
||||||
|
.findAny()
|
||||||
|
.orElseThrow(
|
||||||
|
() -> new IllegalArgumentException(code + " not exists")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package org.example.constant;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品类别</h1>
|
||||||
|
* 电器 -> 手机、电脑
|
||||||
|
* */
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum GoodsCategory {
|
||||||
|
|
||||||
|
DIAN_QI("10001", "电器"),
|
||||||
|
JIA_JU("10002", "家具"),
|
||||||
|
FU_SHI("10003", "服饰"),
|
||||||
|
MY_YIN("10004", "母婴"),
|
||||||
|
SHI_PIN("10005", "食品"),
|
||||||
|
TU_SHU("10006", "图书"),
|
||||||
|
;
|
||||||
|
|
||||||
|
/** 商品类别编码 */
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
/** 商品类别描述信息 */
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>根据 code 获取到 GoodsCategory</h2>
|
||||||
|
* */
|
||||||
|
public static GoodsCategory of(String code) {
|
||||||
|
|
||||||
|
Objects.requireNonNull(code);
|
||||||
|
|
||||||
|
return Stream.of(values())
|
||||||
|
.filter(bean -> bean.code.equals(code))
|
||||||
|
.findAny()
|
||||||
|
.orElseThrow(
|
||||||
|
() -> new IllegalArgumentException(code + " not exists")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.example.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品常量信息</h1>
|
||||||
|
* */
|
||||||
|
public class GoodsConstant {
|
||||||
|
|
||||||
|
/** redis key */
|
||||||
|
public static final String ECOMMERCE_GOODS_DICT_KEY =
|
||||||
|
"ecommerce:goods:dict:20210101";
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package org.example.constant;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品状态枚举类</h1>
|
||||||
|
* */
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum GoodsStatus {
|
||||||
|
|
||||||
|
ONLINE(101, "上线"),
|
||||||
|
OFFLINE(102, "下线"),
|
||||||
|
STOCK_OUT(103, "缺货"),
|
||||||
|
;
|
||||||
|
|
||||||
|
/** 状态码 */
|
||||||
|
private final Integer status;
|
||||||
|
|
||||||
|
/** 状态描述 */
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>根据 code 获取到 GoodsStatus</h2>
|
||||||
|
* */
|
||||||
|
public static GoodsStatus of(Integer status) {
|
||||||
|
|
||||||
|
Objects.requireNonNull(status);
|
||||||
|
|
||||||
|
return Stream.of(values())
|
||||||
|
.filter(bean -> bean.status.equals(status))
|
||||||
|
.findAny()
|
||||||
|
.orElseThrow(
|
||||||
|
() -> new IllegalArgumentException(status + " not exists")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package org.example.controller;
|
||||||
|
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.example.goods.GoodsInfo;
|
||||||
|
import org.example.service.async.AsyncTaskManager;
|
||||||
|
import org.example.vo.AsyncTaskInfo;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>异步任务服务对外提供的 API</h1>
|
||||||
|
* */
|
||||||
|
@Api(tags = "商品异步入库服务")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/async-goods")
|
||||||
|
public class AsyncGoodsController {
|
||||||
|
|
||||||
|
private final AsyncTaskManager asyncTaskManager;
|
||||||
|
|
||||||
|
public AsyncGoodsController(AsyncTaskManager asyncTaskManager) {
|
||||||
|
this.asyncTaskManager = asyncTaskManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "导入商品", notes = "导入商品进入到商品表", httpMethod = "POST")
|
||||||
|
@PostMapping("/import-goods")
|
||||||
|
public AsyncTaskInfo importGoods(@RequestBody List<GoodsInfo> goodsInfos) {
|
||||||
|
return asyncTaskManager.submit(goodsInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "查询状态", notes = "查询异步任务的执行状态", httpMethod = "GET")
|
||||||
|
@GetMapping("/task-info")
|
||||||
|
public AsyncTaskInfo getTaskInfo(@RequestParam String taskId) {
|
||||||
|
return asyncTaskManager.getTaskInfo(taskId);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package org.example.controller;
|
||||||
|
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.example.common.TableId;
|
||||||
|
import org.example.goods.DeductGoodsInventory;
|
||||||
|
import org.example.goods.GoodsInfo;
|
||||||
|
import org.example.goods.SimpleGoodsInfo;
|
||||||
|
import org.example.service.IGoodsService;
|
||||||
|
import org.example.vo.PageSimpleGoodsInfo;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品微服务对外暴露的功能服务 API 接口</h1>
|
||||||
|
* */
|
||||||
|
@Api(tags = "商品微服务功能接口")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/goods")
|
||||||
|
public class GoodsController {
|
||||||
|
|
||||||
|
private final IGoodsService goodsService;
|
||||||
|
|
||||||
|
public GoodsController(IGoodsService goodsService) {
|
||||||
|
this.goodsService = goodsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "详细商品信息", notes = "根据 TableId 查询详细商品信息",
|
||||||
|
httpMethod = "POST")
|
||||||
|
@PostMapping("/goods-info")
|
||||||
|
public List<GoodsInfo> getGoodsInfoByTableId(@RequestBody TableId tableId) {
|
||||||
|
return goodsService.getGoodsInfoByTableId(tableId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "简单商品信息", notes = "获取分页的简单商品信息", httpMethod = "GET")
|
||||||
|
@GetMapping("/page-simple-goods-info")
|
||||||
|
public PageSimpleGoodsInfo getSimpleGoodsInfoByPage(
|
||||||
|
@RequestParam(required = false, defaultValue = "1") int page) {
|
||||||
|
return goodsService.getSimpleGoodsInfoByPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "简单商品信息", notes = "根据 TableId 查询简单商品信息",
|
||||||
|
httpMethod = "POST")
|
||||||
|
@PostMapping("/simple-goods-info")
|
||||||
|
public List<SimpleGoodsInfo> getSimpleGoodsInfoByTableId(@RequestBody TableId tableId) {
|
||||||
|
return goodsService.getSimpleGoodsInfoByTableId(tableId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "扣减商品库存", notes = "扣减商品库存", httpMethod = "PUT")
|
||||||
|
@PutMapping("/deduct-goods-inventory")
|
||||||
|
public Boolean deductGoodsInventory(
|
||||||
|
@RequestBody List<DeductGoodsInventory> deductGoodsInventories) {
|
||||||
|
return goodsService.deductGoodsInventory(deductGoodsInventories);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.example.converter;
|
||||||
|
|
||||||
|
|
||||||
|
import org.example.constant.BrandCategory;
|
||||||
|
|
||||||
|
import javax.persistence.AttributeConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>品牌分类枚举属性转换器</h1>
|
||||||
|
* */
|
||||||
|
public class BrandCategoryConverter implements AttributeConverter<BrandCategory, String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToDatabaseColumn(BrandCategory brandCategory) {
|
||||||
|
return brandCategory.getCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BrandCategory convertToEntityAttribute(String code) {
|
||||||
|
return BrandCategory.of(code);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.example.converter;
|
||||||
|
|
||||||
|
|
||||||
|
import org.example.constant.GoodsCategory;
|
||||||
|
|
||||||
|
import javax.persistence.AttributeConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品类别枚举属性转换器</h1>
|
||||||
|
* */
|
||||||
|
public class GoodsCategoryConverter implements AttributeConverter<GoodsCategory, String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToDatabaseColumn(GoodsCategory goodsCategory) {
|
||||||
|
return goodsCategory.getCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GoodsCategory convertToEntityAttribute(String code) {
|
||||||
|
return GoodsCategory.of(code);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package org.example.converter;
|
||||||
|
|
||||||
|
|
||||||
|
import org.example.constant.GoodsStatus;
|
||||||
|
|
||||||
|
import javax.persistence.AttributeConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品状态枚举属性转换器</h1>
|
||||||
|
* */
|
||||||
|
public class GoodsStatusConverter implements AttributeConverter<GoodsStatus, Integer> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>转换成可以存入数据表的基本类型</h2>
|
||||||
|
* */
|
||||||
|
@Override
|
||||||
|
public Integer convertToDatabaseColumn(GoodsStatus goodsStatus) {
|
||||||
|
return goodsStatus.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>还原数据表中的字段值到 Java 数据类型</h2>
|
||||||
|
* */
|
||||||
|
@Override
|
||||||
|
public GoodsStatus convertToEntityAttribute(Integer status) {
|
||||||
|
return GoodsStatus.of(status);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package org.example.dao;
|
||||||
|
|
||||||
|
|
||||||
|
import org.example.constant.BrandCategory;
|
||||||
|
import org.example.constant.GoodsCategory;
|
||||||
|
import org.example.entity.EcommerceGoods;
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>EcommerceGoods Dao 接口定义</h1>
|
||||||
|
* */
|
||||||
|
public interface EcommerceGoodsDao extends PagingAndSortingRepository<EcommerceGoods, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>根据查询条件查询商品表, 并限制返回结果</h2>
|
||||||
|
* select * from t_dev_protocol_cloud_goods where goods_category = ? and brand_category = ?
|
||||||
|
* and goods_name = ? limit 1;
|
||||||
|
* */
|
||||||
|
Optional<EcommerceGoods> findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName(
|
||||||
|
GoodsCategory goodsCategory, BrandCategory brandCategory,
|
||||||
|
String goodsName
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,162 @@
|
|||||||
|
package org.example.entity;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.example.constant.BrandCategory;
|
||||||
|
import org.example.constant.GoodsCategory;
|
||||||
|
import org.example.constant.GoodsStatus;
|
||||||
|
import org.example.converter.BrandCategoryConverter;
|
||||||
|
import org.example.converter.GoodsCategoryConverter;
|
||||||
|
import org.example.converter.GoodsStatusConverter;
|
||||||
|
import org.example.goods.GoodsInfo;
|
||||||
|
import org.example.goods.SimpleGoodsInfo;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Convert;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EntityListeners;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品表实体类定义</h1>
|
||||||
|
* */
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Entity
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
@Table(name = "t_dev_protocol_cloud_goods")
|
||||||
|
public class EcommerceGoods {
|
||||||
|
|
||||||
|
/** 自增主键 */
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id", nullable = false)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 商品类型 */
|
||||||
|
@Column(name = "goods_category", nullable = false)
|
||||||
|
@Convert(converter = GoodsCategoryConverter.class)
|
||||||
|
private GoodsCategory goodsCategory;
|
||||||
|
|
||||||
|
/** 品牌分类 */
|
||||||
|
@Column(name = "brand_category", nullable = false)
|
||||||
|
@Convert(converter = BrandCategoryConverter.class)
|
||||||
|
private BrandCategory brandCategory;
|
||||||
|
|
||||||
|
/** 商品名称 */
|
||||||
|
@Column(name = "goods_name", nullable = false)
|
||||||
|
private String goodsName;
|
||||||
|
|
||||||
|
/** 商品名称 */
|
||||||
|
@Column(name = "goods_pic", nullable = false)
|
||||||
|
private String goodsPic;
|
||||||
|
|
||||||
|
/** 商品描述信息 */
|
||||||
|
@Column(name = "goods_description", nullable = false)
|
||||||
|
private String goodsDescription;
|
||||||
|
|
||||||
|
/** 商品状态 */
|
||||||
|
@Column(name = "goods_status", nullable = false)
|
||||||
|
@Convert(converter = GoodsStatusConverter.class)
|
||||||
|
private GoodsStatus goodsStatus;
|
||||||
|
|
||||||
|
/** 商品价格: 单位: 分、厘 */
|
||||||
|
@Column(name = "price", nullable = false)
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
/** 总供应量 */
|
||||||
|
@Column(name = "supply", nullable = false)
|
||||||
|
private Long supply;
|
||||||
|
|
||||||
|
/** 库存 */
|
||||||
|
@Column(name = "inventory", nullable = false)
|
||||||
|
private Long inventory;
|
||||||
|
|
||||||
|
/** 商品属性, json 字符串存储 */
|
||||||
|
@Column(name = "goods_property", nullable = false)
|
||||||
|
private String goodsProperty;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "create_time", nullable = false)
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "update_time", nullable = false)
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>将 GoodsInfo 转成实体对象</h2>
|
||||||
|
* */
|
||||||
|
public static EcommerceGoods to(GoodsInfo goodsInfo) {
|
||||||
|
|
||||||
|
EcommerceGoods ecommerceGoods = new EcommerceGoods();
|
||||||
|
|
||||||
|
ecommerceGoods.setGoodsCategory(GoodsCategory.of(goodsInfo.getGoodsCategory()));
|
||||||
|
ecommerceGoods.setBrandCategory(BrandCategory.of(goodsInfo.getBrandCategory()));
|
||||||
|
ecommerceGoods.setGoodsName(goodsInfo.getGoodsName());
|
||||||
|
ecommerceGoods.setGoodsPic(goodsInfo.getGoodsPic());
|
||||||
|
ecommerceGoods.setGoodsDescription(goodsInfo.getGoodsDescription());
|
||||||
|
ecommerceGoods.setGoodsStatus(GoodsStatus.ONLINE); // 可以增加一个审核的过程
|
||||||
|
ecommerceGoods.setPrice(goodsInfo.getPrice());
|
||||||
|
ecommerceGoods.setSupply(goodsInfo.getSupply());
|
||||||
|
ecommerceGoods.setInventory(goodsInfo.getSupply());
|
||||||
|
ecommerceGoods.setGoodsProperty(
|
||||||
|
JSON.toJSONString(goodsInfo.getGoodsProperty())
|
||||||
|
);
|
||||||
|
|
||||||
|
return ecommerceGoods;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>将实体对象转成 GoodsInfo 对象</h2>
|
||||||
|
* */
|
||||||
|
public GoodsInfo toGoodsInfo() {
|
||||||
|
|
||||||
|
GoodsInfo goodsInfo = new GoodsInfo();
|
||||||
|
|
||||||
|
goodsInfo.setId(this.id);
|
||||||
|
goodsInfo.setGoodsCategory(this.goodsCategory.getCode());
|
||||||
|
goodsInfo.setBrandCategory(this.brandCategory.getCode());
|
||||||
|
goodsInfo.setGoodsName(this.goodsName);
|
||||||
|
goodsInfo.setGoodsPic(this.goodsPic);
|
||||||
|
goodsInfo.setGoodsDescription(this.goodsDescription);
|
||||||
|
goodsInfo.setGoodsStatus(this.goodsStatus.getStatus());
|
||||||
|
goodsInfo.setPrice(this.price);
|
||||||
|
goodsInfo.setGoodsProperty(
|
||||||
|
JSON.parseObject(this.goodsProperty, GoodsInfo.GoodsProperty.class)
|
||||||
|
);
|
||||||
|
goodsInfo.setSupply(this.supply);
|
||||||
|
goodsInfo.setInventory(this.inventory);
|
||||||
|
goodsInfo.setCreateTime(this.createTime);
|
||||||
|
goodsInfo.setUpdateTime(this.updateTime);
|
||||||
|
|
||||||
|
return goodsInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>将实体对象转成 SimpleGoodsInfo 对象</h2>
|
||||||
|
* */
|
||||||
|
public SimpleGoodsInfo toSimple() {
|
||||||
|
|
||||||
|
SimpleGoodsInfo goodsInfo = new SimpleGoodsInfo();
|
||||||
|
|
||||||
|
goodsInfo.setId(this.id);
|
||||||
|
goodsInfo.setGoodsName(this.goodsName);
|
||||||
|
goodsInfo.setGoodsPic(this.goodsPic);
|
||||||
|
goodsInfo.setPrice(this.price);
|
||||||
|
|
||||||
|
return goodsInfo;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package org.example.service;
|
||||||
|
|
||||||
|
|
||||||
|
import org.example.common.TableId;
|
||||||
|
import org.example.goods.DeductGoodsInventory;
|
||||||
|
import org.example.goods.GoodsInfo;
|
||||||
|
import org.example.goods.SimpleGoodsInfo;
|
||||||
|
import org.example.vo.PageSimpleGoodsInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品微服务相关服务接口定义</h1>
|
||||||
|
* */
|
||||||
|
public interface IGoodsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>根据 TableId 查询商品详细信息</h2>
|
||||||
|
* */
|
||||||
|
List<GoodsInfo> getGoodsInfoByTableId(TableId tableId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>获取分页的商品信息</h2>
|
||||||
|
* */
|
||||||
|
PageSimpleGoodsInfo getSimpleGoodsInfoByPage(int page);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>根据 TableId 查询简单商品信息</h2>
|
||||||
|
* */
|
||||||
|
List<SimpleGoodsInfo> getSimpleGoodsInfoByTableId(TableId tableId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>扣减商品库存</h2>
|
||||||
|
* */
|
||||||
|
Boolean deductGoodsInventory(List<DeductGoodsInventory> deductGoodsInventories);
|
||||||
|
}
|
@ -0,0 +1,155 @@
|
|||||||
|
package org.example.service.async;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.apache.commons.collections4.IterableUtils;
|
||||||
|
import org.apache.commons.lang3.time.StopWatch;
|
||||||
|
import org.example.constant.GoodsConstant;
|
||||||
|
import org.example.dao.EcommerceGoodsDao;
|
||||||
|
import org.example.entity.EcommerceGoods;
|
||||||
|
import org.example.goods.GoodsInfo;
|
||||||
|
import org.example.goods.SimpleGoodsInfo;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>异步服务接口实现</h1>
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class AsyncServiceImpl implements IAsyncService {
|
||||||
|
|
||||||
|
private final EcommerceGoodsDao ecommerceGoodsDao;
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
public AsyncServiceImpl(EcommerceGoodsDao ecommerceGoodsDao,
|
||||||
|
StringRedisTemplate redisTemplate) {
|
||||||
|
this.ecommerceGoodsDao = ecommerceGoodsDao;
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>异步任务需要加上注解, 并指定使用的线程池</h2>
|
||||||
|
* 异步任务处理两件事:
|
||||||
|
* 1. 将商品信息保存到数据表
|
||||||
|
* 2. 更新商品缓存
|
||||||
|
* */
|
||||||
|
@Async("getAsyncExecutor")
|
||||||
|
@Override
|
||||||
|
public void asyncImportGoods(List<GoodsInfo> goodsInfos, String taskId) {
|
||||||
|
|
||||||
|
log.info("async task running taskId: [{}]", taskId);
|
||||||
|
|
||||||
|
StopWatch watch = StopWatch.createStarted();
|
||||||
|
|
||||||
|
// 1. 如果是 goodsInfo 中存在重复的商品, 不保存; 直接返回, 记录错误日志
|
||||||
|
// 请求数据是否合法的标记
|
||||||
|
boolean isIllegal = false;
|
||||||
|
|
||||||
|
// 将商品信息字段 joint 在一起, 用来判断是否存在重复
|
||||||
|
Set<String> goodsJointInfos = new HashSet<>(goodsInfos.size());
|
||||||
|
// 过滤出来的, 可以入库的商品信息(规则按照自己的业务需求自定义即可)
|
||||||
|
List<GoodsInfo> filteredGoodsInfo = new ArrayList<>(goodsInfos.size());
|
||||||
|
|
||||||
|
// 走一遍循环, 过滤非法参数与判定当前请求是否合法
|
||||||
|
for (GoodsInfo goods : goodsInfos) {
|
||||||
|
|
||||||
|
// 基本条件不满足的, 直接过滤器
|
||||||
|
if (goods.getPrice() <= 0 || goods.getSupply() <= 0) {
|
||||||
|
log.info("goods info is invalid: [{}]", JSON.toJSONString(goods));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合商品信息 - 用于判断唯一性
|
||||||
|
String jointInfo = String.format(
|
||||||
|
"%s,%s,%s",
|
||||||
|
goods.getGoodsCategory(), goods.getBrandCategory(),
|
||||||
|
goods.getGoodsName()
|
||||||
|
);
|
||||||
|
if (goodsJointInfos.contains(jointInfo)) {
|
||||||
|
isIllegal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加入到两个容器中
|
||||||
|
goodsJointInfos.add(jointInfo);
|
||||||
|
filteredGoodsInfo.add(goods);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果存在重复商品或者是没有需要入库的商品, 直接打印日志返回
|
||||||
|
if (isIllegal || CollectionUtils.isEmpty(filteredGoodsInfo)) {
|
||||||
|
watch.stop();
|
||||||
|
log.warn("import nothing: [{}]", JSON.toJSONString(filteredGoodsInfo));
|
||||||
|
log.info("check and import goods done: [{}ms]", watch.getTime(TimeUnit.MILLISECONDS));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EcommerceGoods> ecommerceGoods = filteredGoodsInfo.stream()
|
||||||
|
.map(EcommerceGoods::to)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<EcommerceGoods> targetGoods = new ArrayList<>(ecommerceGoods.size());
|
||||||
|
|
||||||
|
// 2. 保存 goodsInfo 之前先判断下是否存在重复商品
|
||||||
|
ecommerceGoods.forEach(g -> {
|
||||||
|
|
||||||
|
// limit 1
|
||||||
|
if (null != ecommerceGoodsDao
|
||||||
|
.findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName(
|
||||||
|
g.getGoodsCategory(), g.getBrandCategory(),
|
||||||
|
g.getGoodsName()
|
||||||
|
).orElse(null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetGoods.add(g);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 商品信息入库
|
||||||
|
List<EcommerceGoods> savedGoods = IterableUtils.toList(
|
||||||
|
ecommerceGoodsDao.saveAll(targetGoods)
|
||||||
|
);
|
||||||
|
// 将入库商品信息同步到 Redis 中
|
||||||
|
saveNewGoodsInfoToRedis(savedGoods);
|
||||||
|
|
||||||
|
log.info("save goods info to db and redis: [{}]", savedGoods.size());
|
||||||
|
|
||||||
|
watch.stop();
|
||||||
|
log.info("check and import goods success: [{}ms]",
|
||||||
|
watch.getTime(TimeUnit.MILLISECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>将保存到数据表中的数据缓存到 Redis 中</h2>
|
||||||
|
* dict: key -> <id, SimpleGoodsInfo(json)>
|
||||||
|
* */
|
||||||
|
private void saveNewGoodsInfoToRedis(List<EcommerceGoods> savedGoods) {
|
||||||
|
|
||||||
|
// 由于 Redis 是内存存储, 只存储简单商品信息
|
||||||
|
List<SimpleGoodsInfo> simpleGoodsInfos = savedGoods.stream()
|
||||||
|
.map(EcommerceGoods::toSimple)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<String, String> id2JsonObject = new HashMap<>(simpleGoodsInfos.size());
|
||||||
|
simpleGoodsInfos.forEach(
|
||||||
|
g -> id2JsonObject.put(g.getId().toString(), JSON.toJSONString(g))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 保存到 Redis 中
|
||||||
|
redisTemplate.opsForHash().putAll(
|
||||||
|
GoodsConstant.ECOMMERCE_GOODS_DICT_KEY,
|
||||||
|
id2JsonObject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package org.example.service.async;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.example.constant.AsyncTaskStatusEnum;
|
||||||
|
import org.example.goods.GoodsInfo;
|
||||||
|
import org.example.vo.AsyncTaskInfo;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>异步任务执行管理器</h1>
|
||||||
|
* 对异步任务进行包装管理, 记录并塞入异步任务执行信息
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class AsyncTaskManager {
|
||||||
|
|
||||||
|
/** 异步任务执行信息容器 */
|
||||||
|
private final Map<String, AsyncTaskInfo> taskContainer =
|
||||||
|
new HashMap<>(16);
|
||||||
|
|
||||||
|
private final IAsyncService asyncService;
|
||||||
|
|
||||||
|
public AsyncTaskManager(IAsyncService asyncService) {
|
||||||
|
this.asyncService = asyncService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>初始化异步任务</h2>
|
||||||
|
* */
|
||||||
|
public AsyncTaskInfo initTask() {
|
||||||
|
|
||||||
|
AsyncTaskInfo taskInfo = new AsyncTaskInfo();
|
||||||
|
// 设置一个唯一的异步任务 id, 只要唯一即可
|
||||||
|
taskInfo.setTaskId(UUID.randomUUID().toString());
|
||||||
|
taskInfo.setStatus(AsyncTaskStatusEnum.STARTED);
|
||||||
|
taskInfo.setStartTime(new Date());
|
||||||
|
|
||||||
|
// 初始化的时候就要把异步任务执行信息放入到存储容器中
|
||||||
|
taskContainer.put(taskInfo.getTaskId(), taskInfo);
|
||||||
|
return taskInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>提交异步任务</h2>
|
||||||
|
* */
|
||||||
|
public AsyncTaskInfo submit(List<GoodsInfo> goodsInfos) {
|
||||||
|
|
||||||
|
// 初始化一个异步任务的监控信息
|
||||||
|
AsyncTaskInfo taskInfo = initTask();
|
||||||
|
asyncService.asyncImportGoods(goodsInfos, taskInfo.getTaskId());
|
||||||
|
return taskInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>设置异步任务执行状态信息</h2>
|
||||||
|
* */
|
||||||
|
public void setTaskInfo(AsyncTaskInfo taskInfo) {
|
||||||
|
taskContainer.put(taskInfo.getTaskId(), taskInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>获取异步任务执行状态信息</h2>
|
||||||
|
* */
|
||||||
|
public AsyncTaskInfo getTaskInfo(String taskId) {
|
||||||
|
return taskContainer.get(taskId);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package org.example.service.async;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.example.constant.AsyncTaskStatusEnum;
|
||||||
|
import org.example.vo.AsyncTaskInfo;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>异步任务执行监控切面</h1>
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class AsyncTaskMonitor {
|
||||||
|
|
||||||
|
/** 注入异步任务管理器 */
|
||||||
|
private final AsyncTaskManager asyncTaskManager;
|
||||||
|
|
||||||
|
public AsyncTaskMonitor(AsyncTaskManager asyncTaskManager) {
|
||||||
|
this.asyncTaskManager = asyncTaskManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>异步任务执行的环绕切面</h2>
|
||||||
|
* 环绕切面让我们可以在方法执行之前和执行之后做一些 "额外" 的操作
|
||||||
|
* */
|
||||||
|
@Around("execution(* org.example.service.async.AsyncServiceImpl.*(..))")
|
||||||
|
public Object taskHandle(ProceedingJoinPoint proceedingJoinPoint) {
|
||||||
|
|
||||||
|
// 获取 taskId, 调用异步任务传入的第二个参数
|
||||||
|
String taskId = proceedingJoinPoint.getArgs()[1].toString();
|
||||||
|
|
||||||
|
// 获取任务信息, 在提交任务的时候就已经放入到容器中了
|
||||||
|
AsyncTaskInfo taskInfo = asyncTaskManager.getTaskInfo(taskId);
|
||||||
|
log.info("AsyncTaskMonitor is monitoring async task: [{}]", taskId);
|
||||||
|
|
||||||
|
taskInfo.setStatus(AsyncTaskStatusEnum.RUNNING);
|
||||||
|
asyncTaskManager.setTaskInfo(taskInfo); // 设置为运行状态, 并重新放入容器
|
||||||
|
|
||||||
|
AsyncTaskStatusEnum status;
|
||||||
|
Object result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 执行异步任务
|
||||||
|
result = proceedingJoinPoint.proceed();
|
||||||
|
status = AsyncTaskStatusEnum.SUCCESS;
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
// 异步任务出现了异常
|
||||||
|
result = null;
|
||||||
|
status = AsyncTaskStatusEnum.FAILED;
|
||||||
|
log.error("AsyncTaskMonitor: async task [{}] is failed, Error Info: [{}]",
|
||||||
|
taskId, ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置异步任务其他的信息, 再次重新放入到容器中
|
||||||
|
taskInfo.setEndTime(new Date());
|
||||||
|
taskInfo.setStatus(status);
|
||||||
|
taskInfo.setTotalTime(String.valueOf(
|
||||||
|
taskInfo.getEndTime().getTime() - taskInfo.getStartTime().getTime()
|
||||||
|
));
|
||||||
|
asyncTaskManager.setTaskInfo(taskInfo);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package org.example.service.async;
|
||||||
|
|
||||||
|
|
||||||
|
import org.example.goods.GoodsInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>异步服务接口定义</h1>
|
||||||
|
* */
|
||||||
|
public interface IAsyncService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>异步将商品信息保存下来</h2>
|
||||||
|
* */
|
||||||
|
void asyncImportGoods(List<GoodsInfo> goodsInfos, String taskId);
|
||||||
|
}
|
@ -0,0 +1,229 @@
|
|||||||
|
package org.example.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.apache.commons.collections4.IterableUtils;
|
||||||
|
import org.example.common.TableId;
|
||||||
|
import org.example.constant.GoodsConstant;
|
||||||
|
import org.example.dao.EcommerceGoodsDao;
|
||||||
|
import org.example.entity.EcommerceGoods;
|
||||||
|
import org.example.goods.DeductGoodsInventory;
|
||||||
|
import org.example.goods.GoodsInfo;
|
||||||
|
import org.example.goods.SimpleGoodsInfo;
|
||||||
|
import org.example.service.IGoodsService;
|
||||||
|
import org.example.vo.PageSimpleGoodsInfo;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品微服务相关服务功能实现</h1>
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public class GoodsServiceImpl implements IGoodsService {
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final EcommerceGoodsDao ecommerceGoodsDao;
|
||||||
|
|
||||||
|
public GoodsServiceImpl(StringRedisTemplate redisTemplate,
|
||||||
|
EcommerceGoodsDao ecommerceGoodsDao) {
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
this.ecommerceGoodsDao = ecommerceGoodsDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<GoodsInfo> getGoodsInfoByTableId(TableId tableId) {
|
||||||
|
|
||||||
|
// 详细的商品信息, 不能从 redis cache 中去拿
|
||||||
|
List<Long> ids = tableId.getIds().stream()
|
||||||
|
.map(TableId.Id::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.info("get goods info by ids: [{}]", JSON.toJSONString(ids));
|
||||||
|
|
||||||
|
List<EcommerceGoods> ecommerceGoods = IterableUtils.toList(
|
||||||
|
ecommerceGoodsDao.findAllById(ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
return ecommerceGoods.stream()
|
||||||
|
.map(EcommerceGoods::toGoodsInfo).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageSimpleGoodsInfo getSimpleGoodsInfoByPage(int page) {
|
||||||
|
|
||||||
|
// 分页不能从 redis cache 中去拿
|
||||||
|
if (page <= 1) {
|
||||||
|
page = 1; // 默认是第一页
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里分页的规则(你可以自由修改): 1页10条数据, 按照 id 倒序排列
|
||||||
|
Pageable pageable = PageRequest.of(
|
||||||
|
page - 1, 10, Sort.by("id").descending()
|
||||||
|
);
|
||||||
|
Page<EcommerceGoods> orderPage = ecommerceGoodsDao.findAll(pageable);
|
||||||
|
|
||||||
|
// 是否还有更多页: 总页数是否大于当前给定的页
|
||||||
|
boolean hasMore = orderPage.getTotalPages() > page;
|
||||||
|
|
||||||
|
return new PageSimpleGoodsInfo(
|
||||||
|
orderPage.getContent().stream()
|
||||||
|
.map(EcommerceGoods::toSimple).collect(Collectors.toList()),
|
||||||
|
hasMore
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SimpleGoodsInfo> getSimpleGoodsInfoByTableId(TableId tableId) {
|
||||||
|
|
||||||
|
// 获取商品的简单信息, 可以从 redis cache 中去拿, 拿不到需要从 DB 中获取并保存到 Redis 里面
|
||||||
|
// Redis 中的 KV 都是字符串类型
|
||||||
|
List<Object> goodIds = tableId.getIds().stream()
|
||||||
|
.map(i -> i.getId().toString()).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// FIXME 如果 cache 中查不到 goodsId 对应的数据, 返回的是 null, [null, null]
|
||||||
|
List<Object> cachedSimpleGoodsInfos = redisTemplate.opsForHash()
|
||||||
|
.multiGet(GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, goodIds)
|
||||||
|
.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 如果从 Redis 中查到了商品信息, 分两种情况去操作
|
||||||
|
if (CollectionUtils.isNotEmpty(cachedSimpleGoodsInfos)) {
|
||||||
|
// 1. 如果从缓存中查询出所有需要的 SimpleGoodsInfo
|
||||||
|
if (cachedSimpleGoodsInfos.size() == goodIds.size()) {
|
||||||
|
log.info("get simple goods info by ids (from cache): [{}]",
|
||||||
|
JSON.toJSONString(goodIds));
|
||||||
|
return parseCachedGoodsInfo(cachedSimpleGoodsInfos);
|
||||||
|
} else {
|
||||||
|
// 2. 一半从数据表中获取 (right), 一半从 redis cache 中获取 (left)
|
||||||
|
List<SimpleGoodsInfo> left = parseCachedGoodsInfo(cachedSimpleGoodsInfos);
|
||||||
|
// 取差集: 传递进来的参数 - 缓存中查到的 = 缓存中没有的
|
||||||
|
Collection<Long> subtractIds = com.alibaba.nacos.client.naming.utils.CollectionUtils.subtract(
|
||||||
|
goodIds.stream()
|
||||||
|
.map(g -> Long.valueOf(g.toString())).collect(Collectors.toList()),
|
||||||
|
left.stream()
|
||||||
|
.map(SimpleGoodsInfo::getId).collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
// 缓存中没有的, 查询数据表并缓存
|
||||||
|
List<SimpleGoodsInfo> right = queryGoodsFromDBAndCacheToRedis(
|
||||||
|
new TableId(subtractIds.stream().map(TableId.Id::new)
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
);
|
||||||
|
// 合并 left 和 right 并返回
|
||||||
|
log.info("get simple goods info by ids (from db and cache): [{}]",
|
||||||
|
JSON.toJSONString(subtractIds));
|
||||||
|
return new ArrayList<>(CollectionUtils.union(left, right));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 从 redis 里面什么都没有查到
|
||||||
|
return queryGoodsFromDBAndCacheToRedis(tableId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>将缓存中的数据反序列化成 Java Pojo 对象</h2>
|
||||||
|
* */
|
||||||
|
private List<SimpleGoodsInfo> parseCachedGoodsInfo(List<Object> cachedSimpleGoodsInfo) {
|
||||||
|
|
||||||
|
return cachedSimpleGoodsInfo.stream()
|
||||||
|
.map(s -> JSON.parseObject(s.toString(), SimpleGoodsInfo.class))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>从数据表中查询数据, 并缓存到 Redis 中</h2>
|
||||||
|
* */
|
||||||
|
private List<SimpleGoodsInfo> queryGoodsFromDBAndCacheToRedis(TableId tableId) {
|
||||||
|
|
||||||
|
// 从数据表中查询数据并做转换
|
||||||
|
List<Long> ids = tableId.getIds().stream()
|
||||||
|
.map(TableId.Id::getId).collect(Collectors.toList());
|
||||||
|
log.info("get simple goods info by ids (from db): [{}]",
|
||||||
|
JSON.toJSONString(ids));
|
||||||
|
List<EcommerceGoods> ecommerceGoods = IterableUtils.toList(
|
||||||
|
ecommerceGoodsDao.findAllById(ids)
|
||||||
|
);
|
||||||
|
List<SimpleGoodsInfo> result = ecommerceGoods.stream()
|
||||||
|
.map(EcommerceGoods::toSimple).collect(Collectors.toList());
|
||||||
|
// 将结果缓存, 下一次可以直接从 redis cache 中查询
|
||||||
|
log.info("cache goods info: [{}]", JSON.toJSONString(ids));
|
||||||
|
|
||||||
|
Map<String, String> id2JsonObject = new HashMap<>(result.size());
|
||||||
|
result.forEach(g -> id2JsonObject.put(
|
||||||
|
g.getId().toString(), JSON.toJSONString(g)
|
||||||
|
));
|
||||||
|
// 保存到 Redis 中
|
||||||
|
redisTemplate.opsForHash().putAll(
|
||||||
|
GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, id2JsonObject);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean deductGoodsInventory(List<DeductGoodsInventory> deductGoodsInventories) {
|
||||||
|
|
||||||
|
// 检验下参数是否合法
|
||||||
|
deductGoodsInventories.forEach(d -> {
|
||||||
|
if (d.getCount() <= 0) {
|
||||||
|
throw new RuntimeException("purchase goods count need > 0");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List<EcommerceGoods> ecommerceGoods = IterableUtils.toList(
|
||||||
|
ecommerceGoodsDao.findAllById(
|
||||||
|
deductGoodsInventories.stream()
|
||||||
|
.map(DeductGoodsInventory::getGoodsId)
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// 根据传递的 goodsIds 查询不到商品对象, 抛异常
|
||||||
|
if (CollectionUtils.isEmpty(ecommerceGoods)) {
|
||||||
|
throw new RuntimeException("can not found any goods by request");
|
||||||
|
}
|
||||||
|
// 查询出来的商品数量与传递的不一致, 抛异常
|
||||||
|
if (ecommerceGoods.size() != deductGoodsInventories.size()) {
|
||||||
|
throw new RuntimeException("request is not valid");
|
||||||
|
}
|
||||||
|
// goodsId -> DeductGoodsInventory
|
||||||
|
Map<Long, DeductGoodsInventory> goodsId2Inventory =
|
||||||
|
deductGoodsInventories.stream().collect(
|
||||||
|
Collectors.toMap(DeductGoodsInventory::getGoodsId,
|
||||||
|
Function.identity())
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查是不是可以扣减库存, 再去扣减库存
|
||||||
|
ecommerceGoods.forEach(g -> {
|
||||||
|
Long currentInventory = g.getInventory();
|
||||||
|
Integer needDeductInventory = goodsId2Inventory.get(g.getId()).getCount();
|
||||||
|
if (currentInventory < needDeductInventory) {
|
||||||
|
log.error("goods inventory is not enough: [{}], [{}]",
|
||||||
|
currentInventory, needDeductInventory);
|
||||||
|
throw new RuntimeException("goods inventory is not enough: " + g.getId());
|
||||||
|
}
|
||||||
|
// 扣减库存
|
||||||
|
g.setInventory(currentInventory - needDeductInventory);
|
||||||
|
log.info("deduct goods inventory: [{}], [{}], [{}]", g.getId(),
|
||||||
|
currentInventory, g.getInventory());
|
||||||
|
});
|
||||||
|
|
||||||
|
ecommerceGoodsDao.saveAll(ecommerceGoods);
|
||||||
|
log.info("deduct goods inventory done");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.example.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.example.constant.AsyncTaskStatusEnum;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>异步任务执行信息</h1>
|
||||||
|
* */
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AsyncTaskInfo {
|
||||||
|
|
||||||
|
/** 异步任务 id */
|
||||||
|
private String taskId;
|
||||||
|
|
||||||
|
/** 异步任务执行状态 */
|
||||||
|
private AsyncTaskStatusEnum status;
|
||||||
|
|
||||||
|
/** 异步任务开始时间 */
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
/** 异步任务结束时间 */
|
||||||
|
private Date endTime;
|
||||||
|
|
||||||
|
/** 异步任务总耗时 */
|
||||||
|
private String totalTime;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package org.example.vo;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.example.goods.SimpleGoodsInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>分页商品信息</h1>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "分页商品信息对象")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PageSimpleGoodsInfo {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "分页简单商品信息")
|
||||||
|
private List<SimpleGoodsInfo> simpleGoodsInfos;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "是否有更多的商品(分页)")
|
||||||
|
private Boolean hasMore;
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
server:
|
||||||
|
port: 8001
|
||||||
|
servlet:
|
||||||
|
context-path: /dev-protocol-springcloud-project-goods-service
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: dev-protocol-springcloud-project-goods-service
|
||||||
|
cloud:
|
||||||
|
nacos:
|
||||||
|
discovery:
|
||||||
|
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
|
||||||
|
server-addr: 127.0.0.1:8848
|
||||||
|
# server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
|
||||||
|
namespace: 1ccc74ae-9398-4dbe-b9d7-4f9addf9f40c
|
||||||
|
metadata:
|
||||||
|
management:
|
||||||
|
context-path: ${server.servlet.context-path}/actuator
|
||||||
|
kafka:
|
||||||
|
bootstrap-servers: 127.0.0.1:9092
|
||||||
|
producer:
|
||||||
|
retries: 3
|
||||||
|
consumer:
|
||||||
|
auto-offset-reset: latest
|
||||||
|
sleuth:
|
||||||
|
sampler:
|
||||||
|
probability: 1.0 # 采样比例, 1.0 表示 100%, 默认是 0.1
|
||||||
|
zipkin:
|
||||||
|
sender:
|
||||||
|
type: kafka # 默认是 http
|
||||||
|
base-url: http://localhost:9411/
|
||||||
|
jpa:
|
||||||
|
show-sql: true
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: none
|
||||||
|
|
||||||
|
properties:
|
||||||
|
hibernate.show_sql: true
|
||||||
|
hibernate.format_sql: true
|
||||||
|
hibernate:
|
||||||
|
dialect: org.hibernate.dialect.MySQLDialect
|
||||||
|
open-in-view: false
|
||||||
|
datasource:
|
||||||
|
# 数据源
|
||||||
|
url: jdbc:mysql://127.0.0.1:3306/dev_protocol_springcloud_project?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
||||||
|
username: root
|
||||||
|
password: root
|
||||||
|
type: com.zaxxer.hikari.HikariDataSource
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
# 连接池
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 8
|
||||||
|
minimum-idle: 4
|
||||||
|
idle-timeout: 30000
|
||||||
|
connection-timeout: 30000
|
||||||
|
max-lifetime: 45000
|
||||||
|
auto-commit: true
|
||||||
|
pool-name: devProtocolSpringcloudHikariCP
|
||||||
|
redis:
|
||||||
|
database: 0
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
# password:
|
||||||
|
# 连接超时时间
|
||||||
|
timeout: 10000
|
||||||
|
|
||||||
|
# 暴露端点
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: '*'
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
@ -0,0 +1,58 @@
|
|||||||
|
### 导入商品
|
||||||
|
POST http://127.0.0.1:9001/imooc/ecommerce-goods-service/async-goods/import-goods
|
||||||
|
Content-Type: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjAyYTRiZDcxLWUyMTgtNGZmYS1hYzQ3LWE5MGQxNWIzYmEwYSIsImV4cCI6MTYyNDgwOTYwMH0.UWbvqkIq5b5bb-WomLziZyCmjqCqsdeU1EZ0TfWrloRoY7WwqmYGDsf2GnE7JBgVLM0DibhSkkrkXu-wdjzWnqtxLkQ5UgON9BdPm1ZYLvllLcbAMv8KAdbXiC1_FiZ9q1tM6vGXlKU4-G1t88cUUP1_xXOGY9PvC5yGr31lQXCc0Nni4Ds4WwDPHvOq9YBVILdaWYeFsxIWi0pTGwcAxaCkp3BdsvPkJ3uXmrmzuLgkorkfITsmJqdaBuiSCD74LK0F-CvvCv09qizij627O3RuTrpbBfdFjDXT5xyRcKXxAR-n6oFGZdG-JUqh3iXWv_JdsyW-d8wPk3-DZ5zufA
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"goodsCategory": "10001",
|
||||||
|
"brandCategory": "20001",
|
||||||
|
"goodsName": "iphone 11",
|
||||||
|
"goodsPic": "",
|
||||||
|
"goodsDescription": "苹果手机",
|
||||||
|
"price": 100000,
|
||||||
|
"supply": 2000000,
|
||||||
|
"goodsProperty": {
|
||||||
|
"size": "12cm * 6.5cm",
|
||||||
|
"color": "绿色",
|
||||||
|
"material": "金属机身",
|
||||||
|
"pattern": "纯色"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"goodsCategory": "10001",
|
||||||
|
"brandCategory": "20001",
|
||||||
|
"goodsName": "iphone 12",
|
||||||
|
"goodsPic": "",
|
||||||
|
"goodsDescription": "苹果手机",
|
||||||
|
"price": 150000,
|
||||||
|
"supply": 2000000,
|
||||||
|
"goodsProperty": {
|
||||||
|
"size": "12cm * 6.5cm",
|
||||||
|
"color": "绿色",
|
||||||
|
"material": "金属机身",
|
||||||
|
"pattern": "纯色"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"goodsCategory": "10001",
|
||||||
|
"brandCategory": "20001",
|
||||||
|
"goodsName": "iphone 13",
|
||||||
|
"goodsPic": "",
|
||||||
|
"goodsDescription": "苹果手机",
|
||||||
|
"price": 160000,
|
||||||
|
"supply": 2000000,
|
||||||
|
"goodsProperty": {
|
||||||
|
"size": "12cm * 6.5cm",
|
||||||
|
"color": "绿色",
|
||||||
|
"material": "金属机身",
|
||||||
|
"pattern": "纯色"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
### 查询导入商品状态
|
||||||
|
GET http://127.0.0.1:9001/imooc/ecommerce-goods-service/async-goods/task-info?taskId=f5c1c6ff-4efb-45e5-a8c9-9f3d4515228a
|
||||||
|
Accept: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjAyYTRiZDcxLWUyMTgtNGZmYS1hYzQ3LWE5MGQxNWIzYmEwYSIsImV4cCI6MTYyNDgwOTYwMH0.UWbvqkIq5b5bb-WomLziZyCmjqCqsdeU1EZ0TfWrloRoY7WwqmYGDsf2GnE7JBgVLM0DibhSkkrkXu-wdjzWnqtxLkQ5UgON9BdPm1ZYLvllLcbAMv8KAdbXiC1_FiZ9q1tM6vGXlKU4-G1t88cUUP1_xXOGY9PvC5yGr31lQXCc0Nni4Ds4WwDPHvOq9YBVILdaWYeFsxIWi0pTGwcAxaCkp3BdsvPkJ3uXmrmzuLgkorkfITsmJqdaBuiSCD74LK0F-CvvCv09qizij627O3RuTrpbBfdFjDXT5xyRcKXxAR-n6oFGZdG-JUqh3iXWv_JdsyW-d8wPk3-DZ5zufA
|
@ -0,0 +1,57 @@
|
|||||||
|
### 根据 TableId 查询详细商品信息
|
||||||
|
POST http://127.0.0.1:9001/imooc/ecommerce-goods-service/goods/goods-info
|
||||||
|
Content-Type: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A
|
||||||
|
|
||||||
|
{
|
||||||
|
"ids": [
|
||||||
|
{
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### 根据分页查询简单商品信息
|
||||||
|
GET http://127.0.0.1:9001/imooc/ecommerce-goods-service/goods/page-simple-goods-info?page=2
|
||||||
|
Accept: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A
|
||||||
|
|
||||||
|
|
||||||
|
### 根据 TableId 查询简单商品信息: 完整的 goods cache
|
||||||
|
### 第二步验证, 删掉 cache
|
||||||
|
### 第三步验证, 删除 cache 中其中一个商品
|
||||||
|
POST http://127.0.0.1:9001/imooc/ecommerce-goods-service/goods/simple-goods-info
|
||||||
|
Content-Type: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A
|
||||||
|
|
||||||
|
{
|
||||||
|
"ids": [
|
||||||
|
{
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### 扣减商品库存
|
||||||
|
PUT http://127.0.0.1:9001/imooc/ecommerce-goods-service/goods/deduct-goods-inventory
|
||||||
|
Content-Type: application/json
|
||||||
|
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"goodsId": 1,
|
||||||
|
"count": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"goodsId": 2,
|
||||||
|
"count": 34
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
-- 创建 t_ecommerce_goods 数据表
|
||||||
|
CREATE TABLE IF NOT EXISTS `dev_protocol_springcloud_project`.`t_dev_protocol_cloud_goods` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
|
||||||
|
`goods_category` varchar(64) NOT NULL DEFAULT '' COMMENT '商品类别',
|
||||||
|
`brand_category` varchar(64) NOT NULL DEFAULT '' COMMENT '品牌分类',
|
||||||
|
`goods_name` varchar(64) NOT NULL DEFAULT '' COMMENT '商品名称',
|
||||||
|
`goods_pic` varchar(256) NOT NULL DEFAULT '' COMMENT '商品图片',
|
||||||
|
`goods_description` varchar(512) NOT NULL DEFAULT '' COMMENT '商品描述信息',
|
||||||
|
`goods_status` int(11) NOT NULL DEFAULT 0 COMMENT '商品状态',
|
||||||
|
`price` int(11) NOT NULL DEFAULT 0 COMMENT '商品价格',
|
||||||
|
`supply` bigint(20) NOT NULL DEFAULT 0 COMMENT '总供应量',
|
||||||
|
`inventory` bigint(20) NOT NULL DEFAULT 0 COMMENT '库存',
|
||||||
|
`goods_property` varchar(1024) NOT NULL DEFAULT '' COMMENT '商品属性',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `goods_category_brand_name` (`goods_category`, `brand_category`, `goods_name`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
|
@ -0,0 +1,19 @@
|
|||||||
|
package org.example;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品微服务环境可用性校验</h1>
|
||||||
|
* */
|
||||||
|
@SpringBootTest
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
public class GoodsApplicationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contextLoad() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package org.example.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.example.common.TableId;
|
||||||
|
import org.example.goods.DeductGoodsInventory;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品微服务功能测试</h1>
|
||||||
|
* */
|
||||||
|
@Slf4j
|
||||||
|
@SpringBootTest
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
public class GoodsServiceTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IGoodsService goodsService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetGoodsInfoByTableId() {
|
||||||
|
|
||||||
|
List<Long> ids = Arrays.asList(1L, 2L, 3L);
|
||||||
|
List<TableId.Id> tIds = ids.stream()
|
||||||
|
.map(TableId.Id::new).collect(Collectors.toList());
|
||||||
|
log.info("test get goods info by table id: [{}]",
|
||||||
|
JSON.toJSONString(goodsService.getGoodsInfoByTableId(new TableId(tIds))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSimpleGoodsInfoByPage() {
|
||||||
|
|
||||||
|
log.info("test get simple goods info by page: [{}]", JSON.toJSONString(
|
||||||
|
goodsService.getSimpleGoodsInfoByPage(1)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSimpleGoodsInfoByTableId() {
|
||||||
|
|
||||||
|
List<Long> ids = Arrays.asList(1L, 2L, 3L);
|
||||||
|
List<TableId.Id> tIds = ids.stream()
|
||||||
|
.map(TableId.Id::new).collect(Collectors.toList());
|
||||||
|
log.info("test get simple goods info by table id: [{}]", JSON.toJSONString(
|
||||||
|
goodsService.getSimpleGoodsInfoByTableId(new TableId(tIds))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeductGoodsInventory() {
|
||||||
|
|
||||||
|
List<DeductGoodsInventory> deductGoodsInventories = Arrays.asList(
|
||||||
|
new DeductGoodsInventory(1L, 100),
|
||||||
|
new DeductGoodsInventory(2L, 66)
|
||||||
|
);
|
||||||
|
log.info("test deduct goods inventory: [{}]",
|
||||||
|
goodsService.deductGoodsInventory(deductGoodsInventories));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
## 商品微服务
|
||||||
|
|
||||||
|
|
||||||
|
### 商品微服务功能设计
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 商品属性枚举类及转换器定义
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 数据表及 ORM 过程
|
||||||
|
|
||||||
|
|
||||||
|
### 商品信息对象定义及转换方法
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 异步任务与商品服务接口定义
|
@ -0,0 +1,6 @@
|
|||||||
|
# 介绍
|
||||||
|
- 正常服务的通用配置
|
||||||
|
- 开发别的模块也要直接进行导入
|
||||||
|
|
||||||
|
# 已实现功能
|
||||||
|
- 统一身份拦截器
|
@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<artifactId>dev-protocol</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<relativePath>../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>dev-protocol-springcloud-project-service-config</artifactId>
|
||||||
|
|
||||||
|
<!-- 模块名及描述信息 -->
|
||||||
|
<name>dev-protocol-springcloud-project-service-config</name>
|
||||||
|
<description>服务通用配置</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<artifactId>dev-protocol-springcloud-project-mvc-config</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- swagger 用于定义 API 文档 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.springfox</groupId>
|
||||||
|
<artifactId>springfox-swagger2</artifactId>
|
||||||
|
<version>2.9.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.springfox</groupId>
|
||||||
|
<artifactId>springfox-swagger-ui</artifactId>
|
||||||
|
<version>2.9.2</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- 美化 swagger -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
|
<artifactId>swagger-bootstrap-ui</artifactId>
|
||||||
|
<version>1.9.3</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@ -0,0 +1,44 @@
|
|||||||
|
package org.example.conf;
|
||||||
|
|
||||||
|
import org.example.filter.LoginUserInfoInterceptor;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>Web Mvc 配置</h1>
|
||||||
|
* */
|
||||||
|
@Configuration
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class DevProtocolWebMvcConfig extends WebMvcConfigurationSupport {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>添加拦截器配置</h2>
|
||||||
|
* */
|
||||||
|
@Override
|
||||||
|
protected void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
|
||||||
|
// 添加用户身份统一登录拦截的拦截器
|
||||||
|
registry.addInterceptor(new LoginUserInfoInterceptor())
|
||||||
|
.addPathPatterns("/**").order(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>让 MVC 加载 Swagger 的静态资源</h2>
|
||||||
|
* */
|
||||||
|
@Override
|
||||||
|
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
|
||||||
|
registry.addResourceHandler("/**").
|
||||||
|
addResourceLocations("classpath:/static/");
|
||||||
|
registry.addResourceHandler("swagger-ui.html")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/");
|
||||||
|
registry.addResourceHandler("doc.html")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/");
|
||||||
|
registry.addResourceHandler("/webjars/**")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||||
|
|
||||||
|
super.addResourceHandlers(registry);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package org.example.conf;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import springfox.documentation.builders.ApiInfoBuilder;
|
||||||
|
import springfox.documentation.builders.PathSelectors;
|
||||||
|
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||||
|
import springfox.documentation.service.ApiInfo;
|
||||||
|
import springfox.documentation.service.Contact;
|
||||||
|
import springfox.documentation.spi.DocumentationType;
|
||||||
|
import springfox.documentation.spring.web.plugins.Docket;
|
||||||
|
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>Swagger 配置类</h1>
|
||||||
|
* 原生: /swagger-ui.html
|
||||||
|
* 美化: /doc.html
|
||||||
|
* */
|
||||||
|
@Configuration
|
||||||
|
@EnableSwagger2
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class SwaggerConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Swagger 实例 Bean 是 Docket, 所以通过配置 Docket 实例来配置 Swagger</h2>
|
||||||
|
* */
|
||||||
|
@Bean
|
||||||
|
public Docket docket() {
|
||||||
|
|
||||||
|
return new Docket(DocumentationType.SWAGGER_2)
|
||||||
|
// 展示在 Swagger 页面上的自定义工程描述信息
|
||||||
|
.apiInfo(apiInfo())
|
||||||
|
// 选择展示哪些接口
|
||||||
|
.select()
|
||||||
|
// 只有 org.example 包内的才去展示
|
||||||
|
// fixme 这个可以后续进行把一些不展示的接口进行规避
|
||||||
|
.apis(RequestHandlerSelectors.basePackage("org.example"))
|
||||||
|
.paths(PathSelectors.any())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Swagger 的描述信息</h2>
|
||||||
|
* */
|
||||||
|
public ApiInfo apiInfo() {
|
||||||
|
|
||||||
|
return new ApiInfoBuilder()
|
||||||
|
.title("dev-protocol-micro-service")
|
||||||
|
.description("dev-protocol-springcloud-project")
|
||||||
|
.contact(new Contact(
|
||||||
|
"q", "www.q.com", "q@by.com"
|
||||||
|
))
|
||||||
|
.version("1.0")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package org.example.filter;
|
||||||
|
|
||||||
|
import org.example.vo.LoginUserInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>使用 ThreadLocal 去单独存储每一个线程携带的 LoginUserInfo 信息</h1>
|
||||||
|
* 要及时的清理我们保存到 ThreadLocal 中的用户信息:
|
||||||
|
* 1. 保证没有资源泄露
|
||||||
|
* 2. 保证线程在重用时, 不会出现数据混乱
|
||||||
|
* */
|
||||||
|
public class AccessContext {
|
||||||
|
private static final ThreadLocal<LoginUserInfo> loginUserInfo = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public static LoginUserInfo getLoginUserInfo() {
|
||||||
|
return loginUserInfo.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setLoginUserInfo(LoginUserInfo loginUserInfo_) {
|
||||||
|
loginUserInfo.set(loginUserInfo_);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearLoginUserInfo() {
|
||||||
|
loginUserInfo.remove();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package org.example.filter;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.example.constant.CommonConstant;
|
||||||
|
import org.example.utils.TokenParseUtils;
|
||||||
|
import org.example.vo.LoginUserInfo;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户身份统一登录拦截</h1>
|
||||||
|
* */
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class LoginUserInfoInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Object handler) throws Exception {
|
||||||
|
|
||||||
|
// 部分请求不需要带有身份信息, 即白名单
|
||||||
|
if (checkWhiteListUrl(request.getRequestURI())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先尝试从 http header 里面拿到 token
|
||||||
|
String token = request.getHeader(CommonConstant.JWT_USER_INFO_KEY);
|
||||||
|
|
||||||
|
LoginUserInfo loginUserInfo = null;
|
||||||
|
try {
|
||||||
|
loginUserInfo = TokenParseUtils.parseUserInfoFromToken(token);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("parse login user info error: [{}]", ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果程序走到这里, 说明 header 中没有 token 信息
|
||||||
|
if (null == loginUserInfo) {
|
||||||
|
throw new RuntimeException("can not parse current login user");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("set login user info: [{}]", request.getRequestURI());
|
||||||
|
// 设置当前请求上下文, 把用户信息填充进去
|
||||||
|
AccessContext.setLoginUserInfo(loginUserInfo);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postHandle(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Object handler, ModelAndView modelAndView) throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>在请求完全结束后调用, 常用于清理资源等工作</h2>
|
||||||
|
* */
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Object handler, Exception ex) throws Exception {
|
||||||
|
|
||||||
|
if (null != AccessContext.getLoginUserInfo()) {
|
||||||
|
AccessContext.clearLoginUserInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>校验是否是白名单接口</h2>
|
||||||
|
* swagger2 接口
|
||||||
|
* */
|
||||||
|
private boolean checkWhiteListUrl(String url) {
|
||||||
|
|
||||||
|
return StringUtils.containsAny(
|
||||||
|
url,
|
||||||
|
"springfox", "swagger", "v2",
|
||||||
|
"webjars", "doc.html"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
package org.example;
|
@ -0,0 +1,6 @@
|
|||||||
|
# 介绍
|
||||||
|
- 供数据传输的数据结构, 以及转换方式
|
||||||
|
- 开发别的模块也要直接进行导入
|
||||||
|
|
||||||
|
# 已加入实体
|
||||||
|
- 账号微服务 sdk
|
@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<artifactId>dev-protocol</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<relativePath>../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>dev-protocol-springcloud-project-service-sdk</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<!-- 模块名及描述信息 -->
|
||||||
|
<name>dev-protocol-springcloud-project-service-sdk</name>
|
||||||
|
<description>服务模块 SDK</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- swagger 用于定义 API 文档 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.springfox</groupId>
|
||||||
|
<artifactId>springfox-swagger2</artifactId>
|
||||||
|
<version>2.9.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.springfox</groupId>
|
||||||
|
<artifactId>springfox-swagger-ui</artifactId>
|
||||||
|
<version>2.9.2</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- 美化 swagger -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
|
<artifactId>swagger-bootstrap-ui</artifactId>
|
||||||
|
<version>1.9.3</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- lombok 工具通过在代码编译时期动态的将注解替换为具体的代码,
|
||||||
|
IDEA 需要添加 lombok 插件 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@ -0,0 +1,80 @@
|
|||||||
|
package org.example.account;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户地址信息</h1>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "用户地址信息")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AddressInfo {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "地址所属用户 id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "地址详细信息")
|
||||||
|
private List<AddressItem> addressItems;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>单个的地址信息</h2>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "用户的单个地址信息")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class AddressItem {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "地址表主键 id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户姓名")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "电话")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "省")
|
||||||
|
private String province;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "市")
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "详细的地址")
|
||||||
|
private String addressDetail;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "创建时间")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "更新时间")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
public AddressItem(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>将 AddressItem 转换成 UserAddress</h2>
|
||||||
|
* */
|
||||||
|
public UserAddress toUserAddress() {
|
||||||
|
|
||||||
|
UserAddress userAddress = new UserAddress();
|
||||||
|
|
||||||
|
userAddress.setUsername(this.username);
|
||||||
|
userAddress.setPhone(this.phone);
|
||||||
|
userAddress.setProvince(this.province);
|
||||||
|
userAddress.setCity(this.city);
|
||||||
|
userAddress.setAddressDetail(this.addressDetail);
|
||||||
|
|
||||||
|
return userAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package org.example.account;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户账户余额信息</h1>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "用户账户余额信息")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class BalanceInfo {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户主键 id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户账户余额")
|
||||||
|
private Long balance;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.example.account;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>用户地址信息</h1>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "用户地址信息")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserAddress {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户姓名")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "电话")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "省")
|
||||||
|
private String province;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "市")
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "详细的地址")
|
||||||
|
private String addressDetail;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.example.common;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>主键 ids</h1>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "通用 id 对象")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TableId {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "数据表记录主键")
|
||||||
|
private List<Id> ids;
|
||||||
|
|
||||||
|
@ApiModel(description = "数据表记录主键对象")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Id {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "数据表记录主键")
|
||||||
|
private Long id;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package org.example.goods;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>扣减商品库存</h1>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "扣减商品库存对象")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DeductGoodsInventory {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品主键 id")
|
||||||
|
private Long goodsId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "扣减个数")
|
||||||
|
private Integer count;
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package org.example.goods;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>商品信息</h1>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "详细的商品信息")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class GoodsInfo {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品表主键 id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品类别编码")
|
||||||
|
private String goodsCategory;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "品牌分类编码")
|
||||||
|
private String brandCategory;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品名称")
|
||||||
|
private String goodsName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品图片")
|
||||||
|
private String goodsPic;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品描述信息")
|
||||||
|
private String goodsDescription;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品状态")
|
||||||
|
private Integer goodsStatus;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品价格, 单位: 分")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品属性")
|
||||||
|
private GoodsProperty goodsProperty;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "供应量")
|
||||||
|
private Long supply;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "库存")
|
||||||
|
private Long inventory;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "创建时间")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "更新时间")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>商品属性</h2>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "商品属性对象")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class GoodsProperty {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "尺寸")
|
||||||
|
private String size;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "颜色")
|
||||||
|
private String color;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "材质")
|
||||||
|
private String material;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "图案")
|
||||||
|
private String pattern;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package org.example.goods;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>简单的商品信息: 封面</h1>
|
||||||
|
* */
|
||||||
|
@ApiModel(description = "简单的商品信息")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SimpleGoodsInfo {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品表主键 id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品名称")
|
||||||
|
private String goodsName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品图片")
|
||||||
|
private String goodsPic;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "商品价格, 单位: 分")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
public SimpleGoodsInfo(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
package org.example;
|
Loading…
Reference in New Issue