Merge pull request #1 from zihluwang/develop

Enhance Firearm model and add tag filtering to queries
This commit is contained in:
2026-04-09 17:53:28 +08:00
committed by GitHub
8 changed files with 193 additions and 13 deletions
+1 -1
View File
@@ -1 +1 @@
artefactVersion = 1.0.0 artefactVersion = 1.1.0
@@ -15,6 +15,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Validated @Validated
@RestController @RestController
@RequestMapping("/modifications") @RequestMapping("/modifications")
@@ -32,9 +34,10 @@ public class ModificationController {
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size, @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
@RequestParam(required = false) @Positive Long firearmId, @RequestParam(required = false) @Positive Long firearmId,
@RequestParam(defaultValue = "id") String sortBy, @RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "DESC") Sort.Direction direction @RequestParam(defaultValue = "DESC") Sort.Direction direction,
@RequestParam(required = false) List<String> tags
) { ) {
return modificationService.pageQuery(firearmId, PageRequest.of(page, size, Sort.by(direction, sortBy))); return modificationService.pageQuery(firearmId, tags, PageRequest.of(page, size, Sort.by(direction, sortBy)));
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@@ -42,4 +45,3 @@ public class ModificationController {
return modificationService.queryById(id); return modificationService.queryById(id);
} }
} }
@@ -0,0 +1,25 @@
package com.onixbyte.deltaforceguide.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.onixbyte.deltaforceguide.service.ModificationService;
import java.util.List;
@RestController
@RequestMapping("/tags")
public class TagController {
private final ModificationService modificationService;
public TagController(ModificationService modificationService) {
this.modificationService = modificationService;
}
@GetMapping
public List<String> getTags(@RequestParam(required = false) Long firearmId) {
return modificationService.findAllTags(firearmId);
}
}
@@ -8,6 +8,10 @@ public record FirearmResponse(
String name, String name,
FirearmType type, FirearmType type,
String level, String level,
String calibre,
Integer fireRate,
Integer armourDamage,
Integer bodyDamage,
String review String review
) { ) {
public static FirearmResponse from(Firearm firearm) { public static FirearmResponse from(Firearm firearm) {
@@ -16,6 +20,10 @@ public record FirearmResponse(
firearm.getName(), firearm.getName(),
firearm.getType(), firearm.getType(),
firearm.getLevel(), firearm.getLevel(),
firearm.getCalibre(),
firearm.getFireRate(),
firearm.getArmourDamage(),
firearm.getBodyDamage(),
firearm.getReview() firearm.getReview()
); );
} }
@@ -36,6 +36,18 @@ public class Firearm {
@Column(name = "review", columnDefinition = "TEXT") @Column(name = "review", columnDefinition = "TEXT")
private String review; private String review;
@Column(name = "calibre")
private String calibre;
@Column(name = "fire_rate")
private Integer fireRate;
@Column(name = "armour_damage")
private Integer armourDamage;
@Column(name = "body_damage")
private Integer bodyDamage;
@OneToMany(mappedBy = "firearm", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "firearm", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Modification> modifications = new ArrayList<>(); private List<Modification> modifications = new ArrayList<>();
@@ -79,6 +91,38 @@ public class Firearm {
this.review = review; this.review = review;
} }
public String getCalibre() {
return calibre;
}
public void setCalibre(String calibre) {
this.calibre = calibre;
}
public Integer getFireRate() {
return fireRate;
}
public void setFireRate(Integer fireRate) {
this.fireRate = fireRate;
}
public Integer getArmourDamage() {
return armourDamage;
}
public void setArmourDamage(Integer armourDamage) {
this.armourDamage = armourDamage;
}
public Integer getBodyDamage() {
return bodyDamage;
}
public void setBodyDamage(Integer bodyDamage) {
this.bodyDamage = bodyDamage;
}
public List<Modification> getModifications() { public List<Modification> getModifications() {
return modifications; return modifications;
} }
@@ -1,12 +1,16 @@
package com.onixbyte.deltaforceguide.repository; package com.onixbyte.deltaforceguide.repository;
import com.onixbyte.deltaforceguide.domain.entity.Modification; import com.onixbyte.deltaforceguide.domain.entity.Modification;
import org.jspecify.annotations.NonNull;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@@ -20,7 +24,22 @@ public interface ModificationRepository extends JpaRepository<Modification, Long
@Override @Override
@EntityGraph(attributePaths = {"firearm"}) @EntityGraph(attributePaths = {"firearm"})
Optional<Modification> findById(Long id); @NonNull
Optional<Modification> findById(@NonNull Long id);
@Query(value = """
SELECT * FROM modification m
WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId)
AND (CAST(:tagsJson AS text) IS NULL OR cast(m.tags as jsonb) @> cast(CAST(:tagsJson AS text) as jsonb))
""",
countQuery = """
SELECT count(*) FROM modification m
WHERE (:firearmId IS NULL OR m.firearm_id = :firearmId)
AND (CAST(:tagsJson AS text) IS NULL OR cast(m.tags as jsonb) @> cast(CAST(:tagsJson AS text) as jsonb))
""",
nativeQuery = true)
Page<Modification> pageQueryByFirearmAndTags(@Param("firearmId") Long firearmId, @Param("tagsJson") String tagsJson, Pageable pageable);
@Query(value = "SELECT DISTINCT jsonb_array_elements_text(cast(tags as jsonb)) FROM modification WHERE (:firearmId IS NULL OR firearm_id = :firearmId)", nativeQuery = true)
List<String> findAllTags(@Param("firearmId") Long firearmId);
} }
@@ -4,6 +4,8 @@ import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
import com.onixbyte.deltaforceguide.domain.dto.PageResponse; import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
import com.onixbyte.deltaforceguide.domain.entity.Modification; import com.onixbyte.deltaforceguide.domain.entity.Modification;
import com.onixbyte.deltaforceguide.repository.ModificationRepository; import com.onixbyte.deltaforceguide.repository.ModificationRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -11,20 +13,36 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Service @Service
public class ModificationService { public class ModificationService {
private final ModificationRepository modificationRepository; private final ModificationRepository modificationRepository;
private final ObjectMapper objectMapper;
public ModificationService(ModificationRepository modificationRepository) { public ModificationService(ModificationRepository modificationRepository, ObjectMapper objectMapper) {
this.modificationRepository = modificationRepository; this.modificationRepository = modificationRepository;
this.objectMapper = objectMapper;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public PageResponse<ModificationResponse> pageQuery(Long firearmId, Pageable pageable) { public PageResponse<ModificationResponse> pageQuery(Long firearmId, List<String> tags, Pageable pageable) {
Page<Modification> page = firearmId == null String tagsJson = null;
? modificationRepository.findAllBy(pageable) if (tags != null && !tags.isEmpty()) {
: modificationRepository.findAllByFirearm_Id(firearmId, pageable); try {
tagsJson = objectMapper.writeValueAsString(tags);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize tags", e);
}
}
Page<Modification> page;
if (tagsJson != null || firearmId != null) {
page = modificationRepository.pageQueryByFirearmAndTags(firearmId, tagsJson, pageable);
} else {
page = modificationRepository.findAllBy(pageable);
}
return PageResponse.from(page.map(ModificationResponse::from)); return PageResponse.from(page.map(ModificationResponse::from));
} }
@@ -35,5 +53,9 @@ public class ModificationService {
.map(ModificationResponse::from) .map(ModificationResponse::from)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id)); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Modification not found: " + id));
} }
}
@Transactional(readOnly = true)
public List<String> findAllTags(Long firearmId) {
return modificationRepository.findAllTags(firearmId);
}
}
@@ -0,0 +1,60 @@
-- 创建新表
CREATE TABLE firearm_new
(
id BIGSERIAL NOT NULL,
name VARCHAR(64) NOT NULL,
type INTEGER NOT NULL,
level VARCHAR(10) NOT NULL,
calibre VARCHAR(20) NOT NULL,
fire_rate INTEGER NOT NULL,
armour_damage INTEGER NOT NULL,
body_damage INTEGER NOT NULL,
review TEXT NULL,
CONSTRAINT firearm_new_pkey PRIMARY KEY (id)
);
-- 迁移数据
INSERT INTO firearm_new(id, name, type, level, calibre, fire_rate, armour_damage, body_damage,
review)
SELECT id,
name,
type,
level,
calibre,
0,
armour_damage,
body_damage,
review
FROM firearm;
-- 处理外键(关键步骤)
-- 先删除指向旧表的外键约束
ALTER TABLE modification
DROP CONSTRAINT fk_modification_firearm;
-- 重命名旧表和索引
ALTER TABLE firearm
RENAME TO firearm_legacy;
ALTER INDEX firearm_pkey RENAME TO firearm_legacy_pkey;
-- 重命名新表和索引
ALTER TABLE firearm_new
RENAME TO firearm;
ALTER INDEX firearm_new_pkey RENAME TO firearm_pkey;
-- 重新建立外键,指向新的 firearm 表
ALTER TABLE modification
ADD CONSTRAINT fk_modification_firearm
FOREIGN KEY (firearm_id) REFERENCES firearm (id);
-- 序列所有权与名称修正
ALTER SEQUENCE firearm_id_seq RENAME TO firearm_legacy_id_seq;
ALTER SEQUENCE firearm_new_id_seq RENAME TO firearm_id_seq;
ALTER SEQUENCE firearm_id_seq OWNED BY firearm.id;
-- 更新序列计数器
SELECT setval('firearm_id_seq', coalesce(max(id), 1))
FROM firearm;
-- 删除旧表
DROP TABLE IF EXISTS firearm_legacy CASCADE;