Daniel Garcia Costa il y a 3 semaines
Parent
commit
8c77fcfa64

+ 36 - 0
pom.xml

@@ -29,6 +29,19 @@
 	<properties>
 		<java.version>21</java.version>
 	</properties>
+
+	<dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>2025.1.0</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
 	<dependencies>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
@@ -37,6 +50,29 @@
 
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-thymeleaf</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-openfeign</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+			<optional>true</optional>
+		</dependency>
+
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-devtools</artifactId>
 			<scope>runtime</scope>
 			<optional>true</optional>

+ 2 - 0
src/main/java/bidflow/bid/client/ClientApplication.java

@@ -2,8 +2,10 @@ package bidflow.bid.client;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
 
 @SpringBootApplication
+@EnableFeignClients
 public class ClientApplication {
 
 	public static void main(String[] args) {

+ 34 - 0
src/main/java/bidflow/bid/client/client/AuctionClient.java

@@ -0,0 +1,34 @@
+package bidflow.bid.client.client;
+
+import java.util.List;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import bidflow.bid.client.dto.AuctionState;
+import bidflow.bid.client.dto.BidRequest;
+import bidflow.bid.client.dto.CreateAuctionRequest;
+import bidflow.bid.client.dto.ItemDTO;
+
+@FeignClient(name = "auctionClient", url = "${auction.api.url}")
+public interface AuctionClient {
+
+    @PostMapping("/auctions") 
+    void createAuction(@RequestBody CreateAuctionRequest request);
+
+    @PostMapping("/bids")
+    void placeBid(@RequestBody BidRequest request);
+
+    @GetMapping("/auctions/{itemId}")
+    AuctionState getAuction(@PathVariable String itemId);
+
+    @GetMapping("/items")
+    List<ItemDTO> getItems();
+
+    @GetMapping("/items/{itemId}")
+    ItemDTO getItemById(@PathVariable String itemId);
+
+}

+ 18 - 0
src/main/java/bidflow/bid/client/client/CatalogClient.java

@@ -0,0 +1,18 @@
+package bidflow.bid.client.client;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.*;
+
+import bidflow.bid.client.dto.ItemDTO;
+
+import java.util.List;
+
+@FeignClient(name = "catalogClient", url = "${catalog.api.url}")
+public interface CatalogClient {
+
+    @GetMapping("/items")
+    List<ItemDTO> getItems();
+
+    @GetMapping("/items/{id}")
+    ItemDTO getItemById(@PathVariable("id") String id);
+}

+ 15 - 0
src/main/java/bidflow/bid/client/client/PersistenceClient.java

@@ -0,0 +1,15 @@
+package bidflow.bid.client.client;
+
+import java.util.List;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import bidflow.bid.client.dto.Auction;
+
+@FeignClient(name = "persistenceClient", url = "${persistence.api.url}")
+public interface PersistenceClient {
+
+    @GetMapping("/auctions")
+    List<Auction> getHistory();
+}

+ 18 - 0
src/main/java/bidflow/bid/client/client/UsersClient.java

@@ -0,0 +1,18 @@
+package bidflow.bid.client.client;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import bidflow.bid.client.dto.User;
+import bidflow.bid.client.dto.UserResponse;
+
+@FeignClient(name = "usersClient", url = "${users.api.url}")
+public interface UsersClient {
+
+    @GetMapping("/users")
+    UserResponse getUsers();
+
+    @GetMapping("/users/{id}")
+    User getUserById(@PathVariable("id") String id);
+}

+ 111 - 0
src/main/java/bidflow/bid/client/controller/AuctionController.java

@@ -0,0 +1,111 @@
+package bidflow.bid.client.controller;
+
+import bidflow.bid.client.client.AuctionClient;
+import bidflow.bid.client.client.CatalogClient;
+import bidflow.bid.client.client.PersistenceClient;
+import bidflow.bid.client.client.UsersClient;
+import bidflow.bid.client.dto.CreateAuctionRequest;
+import bidflow.bid.client.dto.ItemDTO;
+import bidflow.bid.client.dto.User;
+import bidflow.bid.client.dto.UserResponse;
+import bidflow.bid.client.dto.Auction;
+import bidflow.bid.client.dto.AuctionView;
+import bidflow.bid.client.dto.BidRequest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+@Controller
+public class AuctionController {
+
+    @Autowired
+    private AuctionClient auctionClient;
+    @Autowired
+    private CatalogClient catalogClient;
+    @Autowired
+    private PersistenceClient persistenceClient;
+    @Autowired
+    private UsersClient usersClient;
+
+    @GetMapping("/")
+    public String index(Model model) {
+
+        List<ItemDTO> items = auctionClient.getItems();
+        UserResponse usersResponse = usersClient.getUsers();
+        List<Auction> historyRaw = persistenceClient.getHistory();
+
+        List<User> users = new ArrayList<>();
+        if (usersResponse != null && usersResponse.getEmbedded() != null) {
+            users = usersResponse.getEmbedded().getUsers();
+        }
+
+        // Enriquecer histórico
+        List<AuctionView> history = historyRaw.stream()
+                .map(auction -> {
+
+                    ItemDTO item = catalogClient.getItemById(auction.getItemId());
+
+                    User winner = null;
+                    if (auction.getWinnerUserId() != null && !auction.getWinnerUserId().isEmpty()) {
+                        winner = usersClient.getUserById(auction.getWinnerUserId());
+                    }
+
+                    User seller = null;
+                    if (auction.getSellerId() != null && !auction.getSellerId().isEmpty()) {
+                        seller = usersClient.getUserById(auction.getSellerId());
+                    }
+
+                    String itemName = item != null ? item.getName() : "";
+                    String sellerUsername = seller != null ? seller.getUsername() : "";
+                    String winnerUsername = winner != null ? winner.getUsername() : "";
+
+                    return new AuctionView(
+                            itemName,
+                            sellerUsername,
+                            winnerUsername,
+                            auction.getFinalPrice()
+                    );
+                })
+                .collect(Collectors.toList());
+
+        model.addAttribute("items", items);
+        model.addAttribute("users", users);
+        model.addAttribute("history", history);
+
+        return "index";
+    }
+
+    @PostMapping("/create")
+    public String createAuction(@RequestParam String itemId,
+                               @RequestParam Double startingPrice) {
+
+        CreateAuctionRequest request = new CreateAuctionRequest();
+        request.setItemId(itemId);
+        request.setStartingPrice(startingPrice);
+
+        auctionClient.createAuction(request);
+
+        return "redirect:/";
+    }
+
+    @PostMapping("/bid")
+    public String placeBid(@RequestParam String itemId,
+                          @RequestParam String userId,
+                          @RequestParam Double amount) {
+
+        BidRequest request = new BidRequest();
+        request.setItemId(itemId);
+        request.setUserId(userId);
+        request.setAmount(amount);
+
+        auctionClient.placeBid(request);
+
+        return "redirect:/";
+    }
+}

+ 33 - 0
src/main/java/bidflow/bid/client/dto/Auction.java

@@ -0,0 +1,33 @@
+package bidflow.bid.client.dto;
+
+import java.util.List;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class Auction {
+
+    private String auctionId;
+    private String itemId;
+
+    private String itemName;
+    private String categoryName;
+
+    private String sellerId;
+    private String sellerUsername;
+
+    private String winnerUserId;
+    private String winnerUsername;
+
+    private Double finalPrice;
+
+    private List<Bid> bids;
+}
+
+

+ 25 - 0
src/main/java/bidflow/bid/client/dto/AuctionState.java

@@ -0,0 +1,25 @@
+package bidflow.bid.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.Instant;
+import java.util.List;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuctionState {
+
+    private String itemId;
+    private Double currentPrice;
+    private String winnerUserId;
+
+    private List<Bid> bids;
+
+    private Instant startTime;
+    private Instant endTime;
+}

+ 15 - 0
src/main/java/bidflow/bid/client/dto/AuctionView.java

@@ -0,0 +1,15 @@
+package bidflow.bid.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class AuctionView {
+
+    private String sellerUsername;
+    private String winnerUsername;    
+    private String itemName;
+    private Double finalPrice;
+}
+    

+ 19 - 0
src/main/java/bidflow/bid/client/dto/Bid.java

@@ -0,0 +1,19 @@
+package bidflow.bid.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class Bid {
+
+    private String userId;
+    private Double amount;
+    private Instant timestamp;
+}

+ 17 - 0
src/main/java/bidflow/bid/client/dto/BidRequest.java

@@ -0,0 +1,17 @@
+package bidflow.bid.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class BidRequest {
+    private String itemId;
+    private String userId;
+    private Double amount;
+
+}

+ 16 - 0
src/main/java/bidflow/bid/client/dto/CreateAuctionRequest.java

@@ -0,0 +1,16 @@
+package bidflow.bid.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateAuctionRequest {
+
+    private String itemId;
+    private Double startingPrice;
+}

+ 17 - 0
src/main/java/bidflow/bid/client/dto/ItemDTO.java

@@ -0,0 +1,17 @@
+package bidflow.bid.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class ItemDTO {
+
+    private String id;
+    private String name;
+    private Double startingPrice;
+}

+ 15 - 0
src/main/java/bidflow/bid/client/dto/User.java

@@ -0,0 +1,15 @@
+package bidflow.bid.client.dto;
+
+import lombok.*;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+
+    private String id;
+    private String username;
+    private String email;
+
+}

+ 30 - 0
src/main/java/bidflow/bid/client/dto/UserResponse.java

@@ -0,0 +1,30 @@
+package bidflow.bid.client.dto;
+
+import java.util.List;
+
+import lombok.*;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class UserResponse {
+
+    @JsonProperty("_embedded")
+    private Embedded embedded;
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Embedded {
+
+        private List<User> users;
+    }
+}
+

+ 22 - 0
src/main/resources/META-INF/additional-spring-configuration-metadata.json

@@ -0,0 +1,22 @@
+{"properties": [
+  {
+    "name": "auction.api.url",
+    "type": "java.lang.String",
+    "description": "URL de la API de subastas"
+  },
+  {
+    "name": "users.api.url",
+    "type": "java.lang.String",
+    "description": "URL de la API de usuarios"
+  },
+  {
+    "name": "persistence.api.url",
+    "type": "java.lang.String",
+    "description": "URL de la API de persistencia mongo"
+  },
+  {
+    "name": "catalog.api.url",
+    "type": "java.lang.String",
+    "description": "URL de la API de persistencia catalog (relacional)"
+  }
+]}

+ 8 - 0
src/main/resources/application.properties

@@ -1 +1,9 @@
 spring.application.name=client
+server.port=8080
+
+
+auction.api.url=http://localhost:8081
+catalog.api.url=http://localhost:8082/api
+users.api.url=http://localhost:8083/api
+persistence.api.url=http://localhost:8084
+

+ 171 - 0
src/main/resources/templates/index.html

@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+<head>
+    <title>Plataforma de Subastas</title>
+    <meta charset="UTF-8">
+
+    <!-- Bootstrap -->
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
+</head>
+
+<body class="bg-light">
+
+<div class="container py-5">
+
+    <!-- Título -->
+    <div class="text-center mb-4">
+        <h1 class="fw-bold">Plataforma de Subastas</h1>
+        <p class="text-muted">Sistema distribuido con Redis y MongoDB</p>
+    </div>
+
+    <!-- Crear Subasta -->
+    <div class="card mb-4 shadow-sm">
+        <div class="card-header bg-primary text-white">
+            Crear Subasta
+        </div>
+        <div class="card-body">
+
+            <form action="/create" method="post" class="row g-3">
+
+                <!-- Selector de Item -->
+                <div class="col-md-8">
+                    <label class="form-label">Seleccionar Item</label>
+                    <select name="itemId" class="form-select" required>
+                        <option value="">-- Seleccionar item --</option>
+                        <option th:each="item : ${items}"
+                                th:value="${item.id}"
+                                th:text="${item.name}">
+                        </option>
+                    </select>
+                </div>
+
+                <!-- Precio -->
+                <div class="col-md-3">
+                    <label class="form-label">Precio inicial</label>
+                    <input type="number" step="0.01" name="startingPrice" class="form-control" required>
+                </div>
+
+                <!-- Botón -->
+                <div class="col-md-1 d-flex align-items-end">
+                    <button type="submit" class="btn btn-primary w-100">
+                        Crear
+                    </button>
+                </div>
+
+            </form>
+
+        </div>
+    </div>
+
+    <!-- Pujar -->
+    <div class="card mb-4 shadow-sm">
+        <div class="card-header bg-success text-white">
+            Pujar
+        </div>
+        <div class="card-body">
+
+            <form action="/bid" method="post" class="row g-3">
+
+                <!-- Selector de Item -->
+                <div class="col-md-4">
+                    <label class="form-label">Item</label>
+                    <select name="itemId" class="form-select" required>
+                        <option value="">-- Seleccionar item --</option>
+                        <option th:each="item : ${items}"
+                                th:value="${item.id}"
+                                th:text="${item.name}">
+                        </option>
+                    </select>
+                </div>
+
+                <!-- Selector de Usuario -->
+                <div class="col-md-4">
+                    <label class="form-label">Usuario</label>
+                    <select name="userId" class="form-select" required>
+                        <option value="">-- Seleccionar usuario --</option>
+                        <option th:each="user : ${users}"
+                                th:value="${user.id}"
+                                th:text="${user.username}">
+                        </option>
+                    </select>
+                </div>
+
+                <!-- Cantidad -->
+                <div class="col-md-3">
+                    <label class="form-label">Cantidad</label>
+                    <input type="number" step="0.01" name="amount" class="form-control" required>
+                </div>
+
+                <!-- Botón -->
+                <div class="col-md-1 d-flex align-items-end">
+                    <button type="submit" class="btn btn-success w-100">
+                        Pujar
+                    </button>
+                </div>
+
+            </form>
+
+        </div>
+    </div>
+
+    <!-- Items -->
+    <div class="card mb-4 shadow-sm">
+        <div class="card-header">
+            Items disponibles
+        </div>
+        <div class="card-body">
+
+            <table class="table table-striped">
+                <thead>
+                <tr>
+                    <th>Nombre</th>
+                    <th>ID</th>
+                </tr>
+                </thead>
+
+                <tbody>
+                <tr th:each="item : ${items}">
+                    <td th:text="${item.name}"></td>
+                    <td th:text="${item.id}"></td>
+                </tr>
+                </tbody>
+            </table>
+
+        </div>
+    </div>
+
+    <!-- Histórico -->
+    <div class="card shadow-sm">
+        <div class="card-header">
+            Histórico de subastas
+        </div>
+        <div class="card-body">
+
+            <table class="table table-bordered">
+                <thead>
+                <tr>
+                    <th>Item</th>
+                    <th>Vendedor</th>
+                    <th>Ganador</th>
+                    <th>Precio final</th>
+                </tr>
+                </thead>
+
+                <tbody>
+                <tr th:each="auction : ${history}">
+                    <td th:text="${auction.itemName}"></td>
+                    <td th:text="${auction.sellerUsername}"></td>
+                    <td th:text="${auction.winnerUsername}"></td>
+                    <td th:text="${auction.finalPrice}"></td>
+                </tr>
+                </tbody>
+
+            </table>
+
+        </div>
+    </div>
+
+</div>
+
+</body>
+</html>