diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/ModificationController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/ModificationController.java index 7eaf94c..8c68b87 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/ModificationController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/ModificationController.java @@ -4,7 +4,9 @@ import com.onixbyte.deltaforceguide.domain.dto.ModificationBatchCreateRequest; import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest; import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse; import com.onixbyte.deltaforceguide.domain.dto.PageResponse; +import com.onixbyte.deltaforceguide.domain.entity.User; import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth; +import com.onixbyte.deltaforceguide.security.resolver.CurrentUser; import com.onixbyte.deltaforceguide.service.ModificationService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -66,15 +68,21 @@ public class ModificationController { @RequiresAuth @Operation(description = "创建改装") @PostMapping - public ModificationResponse create(@Valid @RequestBody ModificationRequest request) { - return modificationService.create(request); + public ModificationResponse create( + @CurrentUser User user, + @Valid @RequestBody ModificationRequest request + ) { + return modificationService.create(request, user); } @RequiresAuth @Operation(description = "批量创建改装") @PostMapping("/batch") - public List batchCreate(@Valid @RequestBody ModificationBatchCreateRequest request) { - return modificationService.batchCreate(request.modifications()); + public List batchCreate( + @CurrentUser User user, + @Valid @RequestBody ModificationBatchCreateRequest request + ) { + return modificationService.batchCreate(request.modifications(), user); } @RequiresAuth diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/converter/ModificationStatusConverter.java b/src/main/java/com/onixbyte/deltaforceguide/domain/converter/ModificationStatusConverter.java new file mode 100644 index 0000000..5e5bab5 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/converter/ModificationStatusConverter.java @@ -0,0 +1,24 @@ +package com.onixbyte.deltaforceguide.domain.converter; + +import com.onixbyte.deltaforceguide.enumeration.ModificationStatus; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +/** + * JPA attribute converter that maps {@link ModificationStatus} enum to/from its integer database representation. + * + * @author zihluwang + */ +@Converter +public class ModificationStatusConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(ModificationStatus attribute) { + return attribute == null ? null : attribute.getCode(); + } + + @Override + public ModificationStatus convertToEntityAttribute(Integer dbData) { + return ModificationStatus.fromCode(dbData); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Modification.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Modification.java index a1942f7..9780741 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Modification.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/Modification.java @@ -1,11 +1,15 @@ package com.onixbyte.deltaforceguide.domain.entity; +import com.onixbyte.deltaforceguide.domain.converter.ModificationStatusConverter; +import com.onixbyte.deltaforceguide.enumeration.ModificationStatus; import io.hypersistence.utils.hibernate.type.json.JsonType; import jakarta.persistence.*; import org.hibernate.annotations.Type; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * Entity representing a firearm modification or build configuration. @@ -14,10 +18,10 @@ import java.util.List; */ @Entity @Table( - name = "modification", - indexes = { - @Index(name = "idx_modification_firearm_id", columnList = "firearm_id") - } + name = "modification", + indexes = { + @Index(name = "idx_modification_firearm_id", columnList = "firearm_id") + } ) public class Modification { @@ -25,6 +29,10 @@ public class Modification { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "firearm_id", nullable = false, foreignKey = @ForeignKey(name = "fk_modification_firearm")) private Firearm firearm; @@ -36,11 +44,11 @@ public class Modification { private String code; @Type(JsonType.class) - @Column(name = "tags", columnDefinition = "jsonb") + @Column(name = "tags", columnDefinition = "jsonb", nullable = false) private List tags = new ArrayList<>(); @Type(JsonType.class) - @Column(name = "accessories", columnDefinition = "jsonb") + @Column(name = "accessories", columnDefinition = "jsonb", nullable = false) private List accessories = new ArrayList<>(); @Column(name = "note", columnDefinition = "TEXT") @@ -52,6 +60,16 @@ public class Modification { @Column(name = "video_url", length = 512) private String videoUrl; + @Column(name = "status", nullable = false) + @Convert(converter = ModificationStatusConverter.class) + private ModificationStatus status = ModificationStatus.DRAFT; + + @Column(name = "create_by") + private Long createBy; + + @Column(name = "create_time", nullable = false) + private OffsetDateTime createTime; + public Long getId() { return id; } @@ -132,6 +150,38 @@ public class Modification { this.videoUrl = videoUrl; } + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Long getCreateBy() { + return createBy; + } + + public void setCreateBy(Long createBy) { + this.createBy = createBy; + } + + public OffsetDateTime getCreateTime() { + return createTime; + } + + public void setCreateTime(OffsetDateTime createTime) { + this.createTime = createTime; + } + + public ModificationStatus getStatus() { + return status; + } + + public void setStatus(ModificationStatus status) { + this.status = status; + } + public static Builder builder() { return new Builder(); } @@ -139,6 +189,7 @@ public class Modification { public static class Builder { private Long id; + private User user; private Firearm firearm; private String name; private String code; @@ -147,12 +198,20 @@ public class Modification { private String note; private String author; private String videoUrl; + private Long createBy; + private OffsetDateTime createTime; + private ModificationStatus status; public Builder id(Long id) { this.id = id; return this; } + public Builder user(User user) { + this.user = user; + return this; + } + public Builder firearm(Firearm firearm) { this.firearm = firearm; return this; @@ -193,17 +252,36 @@ public class Modification { return this; } + public Builder createBy(Long createBy) { + this.createBy = createBy; + return this; + } + + public Builder createTime(OffsetDateTime createTime) { + this.createTime = createTime; + return this; + } + + public Builder status(ModificationStatus status) { + this.status = status; + return this; + } + public Modification build() { Modification modification = new Modification(); modification.id = this.id; + modification.user = this.user; modification.firearm = this.firearm; modification.name = this.name; modification.code = this.code; - modification.tags = this.tags == null ? new ArrayList<>() : this.tags; - modification.accessories = this.accessories == null ? new ArrayList<>() : this.accessories; + modification.tags = Optional.ofNullable(this.tags).orElseGet(ArrayList::new); + modification.accessories = Optional.ofNullable(this.accessories).orElseGet(ArrayList::new); modification.note = this.note; modification.author = this.author; modification.videoUrl = this.videoUrl; + modification.createBy = this.createBy; + modification.createTime = Optional.ofNullable(this.createTime).orElseGet(OffsetDateTime::now); + modification.status = Optional.ofNullable(this.status).orElse(ModificationStatus.DRAFT); return modification; } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserRole.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserRole.java index 8caf84e..07bb6dc 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserRole.java +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserRole.java @@ -1,12 +1,10 @@ package com.onixbyte.deltaforceguide.domain.entity; -import jakarta.persistence.AttributeOverride; -import jakarta.persistence.AttributeOverrides; import jakarta.persistence.Column; -import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.ForeignKey; +import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.MapsId; @@ -14,6 +12,8 @@ import jakarta.persistence.Table; /** * Entity representing a role assignment linking a user to a security role. + *

+ * A user owns at most one role assignment, so the user ID is the primary key. * * @author zihluwang */ @@ -21,24 +21,24 @@ import jakarta.persistence.Table; @Table(name = "user_role") public class UserRole { - @EmbeddedId - @AttributeOverrides({ - @AttributeOverride(name = "userId", column = @Column(name = "user_id")), - @AttributeOverride(name = "role", column = @Column(name = "role")) - }) - private UserRoleId id = new UserRoleId(); + @Id + @Column(name = "user_id") + private Long userId; @MapsId("userId") @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_role_user")) private User user; - public UserRoleId getId() { - return id; + @Column(name = "role", nullable = false, length = 32) + private String role; + + public Long getUserId() { + return userId; } - public void setId(UserRoleId id) { - this.id = id; + public void setUserId(Long userId) { + this.userId = userId; } public User getUser() { @@ -47,32 +47,17 @@ public class UserRole { public void setUser(User user) { this.user = user; - if (this.id == null) { - this.id = new UserRoleId(); + if (user != null) { + this.userId = user.getId(); } - this.id.setUserId(user == null ? null : user.getId()); - } - - public Long getUserId() { - return id == null ? null : id.getUserId(); - } - - public void setUserId(Long userId) { - if (this.id == null) { - this.id = new UserRoleId(); - } - this.id.setUserId(userId); } public String getRole() { - return id == null ? null : id.getRole(); + return role; } public void setRole(String role) { - if (this.id == null) { - this.id = new UserRoleId(); - } - this.id.setRole(role); + this.role = role; } public static Builder builder() { @@ -81,16 +66,10 @@ public class UserRole { public static class Builder { - private UserRoleId id; private User user; private Long userId; private String role; - public Builder id(UserRoleId id) { - this.id = id; - return this; - } - public Builder user(User user) { this.user = user; return this; @@ -108,16 +87,13 @@ public class UserRole { public UserRole build() { UserRole userRole = new UserRole(); - userRole.id = this.id == null ? new UserRoleId() : this.id; userRole.user = this.user; + userRole.role = this.role; if (this.user != null) { - userRole.id.setUserId(this.user.getId()); + userRole.userId = this.user.getId(); } if (this.userId != null) { - userRole.id.setUserId(this.userId); - } - if (this.role != null) { - userRole.id.setRole(this.role); + userRole.userId = this.userId; } return userRole; } diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserRoleId.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserRoleId.java deleted file mode 100644 index 96f5b0e..0000000 --- a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserRoleId.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.onixbyte.deltaforceguide.domain.entity; - -import jakarta.persistence.Embeddable; - -import java.io.Serializable; -import java.util.Objects; - -/** - * Composite key for the UserRole entity, combining user ID and role name. - * - * @author zihluwang - */ -@Embeddable -public class UserRoleId implements Serializable { - - private Long userId; - - private String role; - - public Long getUserId() { - return userId; - } - - public void setUserId(Long userId) { - this.userId = userId; - } - - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - UserRoleId that = (UserRoleId) o; - return Objects.equals(userId, that.userId) && Objects.equals(role, that.role); - } - - @Override - public int hashCode() { - return Objects.hash(userId, role); - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private Long userId; - private String role; - - public Builder userId(Long userId) { - this.userId = userId; - return this; - } - - public Builder role(String role) { - this.role = role; - return this; - } - - public UserRoleId build() { - UserRoleId id = new UserRoleId(); - id.userId = this.userId; - id.role = this.role; - return id; - } - } -} diff --git a/src/main/java/com/onixbyte/deltaforceguide/enumeration/ModificationStatus.java b/src/main/java/com/onixbyte/deltaforceguide/enumeration/ModificationStatus.java new file mode 100644 index 0000000..5699405 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/enumeration/ModificationStatus.java @@ -0,0 +1,44 @@ +package com.onixbyte.deltaforceguide.enumeration; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Enumeration of modification statuses. + *

+ * Each status is associated with an integer code used for database persistence. + * + * @author zihluwang + */ +public enum ModificationStatus { + + DRAFT(0), + PENDING_APPROVAL(1), + PUBLISHED(2); + + private final int code; + + ModificationStatus(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static final Map CODE_MAP = Arrays.stream(values()) + .collect(Collectors.toMap(ModificationStatus::getCode, Function.identity())); + + /** + * Resolve a ModificationStatus from its integer code. + * + * @param code the integer code, may be null + * @return the corresponding ModificationStatus, or null if the code is null + * @throws IllegalArgumentException if the code does not match any known status + */ + public static ModificationStatus fromCode(Integer code) { + return CODE_MAP.get(code); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java index f7defab..8dfdc92 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/ModificationManager.java @@ -4,6 +4,7 @@ import com.onixbyte.deltaforceguide.domain.entity.Accessory; import com.onixbyte.deltaforceguide.domain.entity.Firearm; import com.onixbyte.deltaforceguide.domain.entity.Modification; import com.onixbyte.deltaforceguide.domain.entity.Tuning; +import com.onixbyte.deltaforceguide.domain.entity.User; import com.onixbyte.deltaforceguide.domain.dto.AccessoryRequest; import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest; import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse; @@ -36,16 +37,16 @@ public class ModificationManager { } @Transactional - public ModificationResponse create(ModificationRequest request) { + public ModificationResponse create(ModificationRequest request, User user) { var firearm = firearmRepository.findById(request.firearmId()) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Firearm not found: " + request.firearmId())); - var modification = toEntity(request, firearm); + var modification = toEntity(request, firearm, user); return ModificationResponse.from(modificationRepository.save(modification)); } @Transactional - public List batchCreate(List requests) { + public List batchCreate(List requests, User user) { var firearmIds = requests.stream() .map(ModificationRequest::firearmId) .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); @@ -63,7 +64,7 @@ public class ModificationManager { } var modifications = requests.stream() - .map(req -> toEntity(req, firearmMap.get(req.firearmId()))) + .map(req -> toEntity(req, firearmMap.get(req.firearmId()), user)) .toList(); return modificationRepository.saveAll(modifications) .stream() @@ -86,7 +87,7 @@ public class ModificationManager { return matches.getFirst().getId(); } - private Modification toEntity(ModificationRequest request, Firearm firearm) { + private Modification toEntity(ModificationRequest request, Firearm firearm, User user) { return Modification.builder() .firearm(firearm) .name(request.name()) @@ -96,6 +97,7 @@ public class ModificationManager { .note(request.note()) .author(request.author()) .videoUrl(request.videoUrl()) + .createBy(user == null ? null : user.getId()) .build(); } diff --git a/src/main/java/com/onixbyte/deltaforceguide/repository/UserRoleRepository.java b/src/main/java/com/onixbyte/deltaforceguide/repository/UserRoleRepository.java index 16f0792..62368b2 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/repository/UserRoleRepository.java +++ b/src/main/java/com/onixbyte/deltaforceguide/repository/UserRoleRepository.java @@ -1,7 +1,6 @@ package com.onixbyte.deltaforceguide.repository; import com.onixbyte.deltaforceguide.domain.entity.UserRole; -import com.onixbyte.deltaforceguide.domain.entity.UserRoleId; import org.springframework.data.jpa.repository.*; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -14,7 +13,7 @@ import java.util.List; * @author zihluwang */ @Repository -public interface UserRoleRepository extends JpaRepository, JpaSpecificationExecutor { +public interface UserRoleRepository extends JpaRepository, JpaSpecificationExecutor { /** * Find all roles assigned to a given user. @@ -41,7 +40,7 @@ public interface UserRoleRepository extends JpaRepository, select count(ur) > 0 from UserRole ur where ur.user.id = :userId - and ur.id.role = :role + and ur.role = :role """) boolean existsByUserIdAndRole(@Param("userId") Long userId, @Param("role") String role); @@ -55,7 +54,7 @@ public interface UserRoleRepository extends JpaRepository, @Query(""" delete from UserRole ur where ur.user.id = :userId - and ur.id.role = :role + and ur.role = :role """) void deleteByUserIdAndRole(@Param("userId") Long userId, @Param("role") String role); diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java b/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java index 9b97e8f..d2f27c5 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/ModificationService.java @@ -8,6 +8,7 @@ import com.onixbyte.deltaforceguide.domain.dto.TuningRequest; import com.onixbyte.deltaforceguide.domain.entity.Accessory; import com.onixbyte.deltaforceguide.domain.entity.Firearm; import com.onixbyte.deltaforceguide.domain.entity.Modification; +import com.onixbyte.deltaforceguide.domain.entity.User; import com.onixbyte.deltaforceguide.domain.entity.Tuning; import com.onixbyte.deltaforceguide.manager.ModificationManager; import com.onixbyte.deltaforceguide.repository.FirearmRepository; @@ -104,20 +105,22 @@ public class ModificationService { * Creates a new modification for a given firearm. * * @param request the modification creation request + * @param user the authenticated user creating the modification * @return the created modification response */ - public ModificationResponse create(ModificationRequest request) { - return modificationManager.create(request); + public ModificationResponse create(ModificationRequest request, User user) { + return modificationManager.create(request, user); } /** * Creates multiple modifications in a single batch operation. * * @param requests list of modification creation requests + * @param user the authenticated user creating the modifications * @return list of created modification responses */ - public List batchCreate(List requests) { - return modificationManager.batchCreate(requests); + public List batchCreate(List requests, User user) { + return modificationManager.batchCreate(requests, user); } /** diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java index 6b7810e..c922b9f 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/WebhookService.java @@ -88,7 +88,7 @@ public class WebhookService { private void processSingle(Long issueNumber, Map data) { var request = mapToRequest(data); log.info("Creating modification from issue #{}: name={}", issueNumber, request.name()); - modificationManager.create(request); + modificationManager.create(request, null); } @SuppressWarnings("unchecked") @@ -102,7 +102,7 @@ public class WebhookService { .map(this::mapToRequest) .toList(); log.info("Batch creating {} modifications from issue #{}", requests.size(), issueNumber); - modificationManager.batchCreate(requests); + modificationManager.batchCreate(requests, null); } private ModificationRequest mapToRequest(Map data) { diff --git a/src/main/resources/db/migration/V8__modification.sql b/src/main/resources/db/migration/V8__modification.sql new file mode 100644 index 0000000..eba22e7 --- /dev/null +++ b/src/main/resources/db/migration/V8__modification.sql @@ -0,0 +1,71 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS modification_new +( + id BIGSERIAL NOT NULL PRIMARY KEY, + user_id BIGINT NULL REFERENCES app_user (id), + firearm_id BIGINT NOT NULL REFERENCES firearm (id), + name VARCHAR(64) NOT NULL, + code VARCHAR(64) NOT NULL, + tags JSONB NOT NULL DEFAULT '[]'::JSONB, + note TEXT NULL, + author VARCHAR(64) NULL, + video_url VARCHAR(512) NULL, + accessories JSONB NOT NULL DEFAULT '[]'::JSONB, + status INTEGER NOT NULL DEFAULT 0, + create_by BIGINT NULL, + create_time TIMESTAMPTZ NOT NULL, + CONSTRAINT tags_must_be_string_array CHECK ( + jsonb_typeof(tags) = 'array' AND + NOT jsonb_path_exists(tags, '$[*] ? (@.type() != "string")') + ) +); + +DO +$$ + DECLARE + v_super_user_id BIGINT; + BEGIN + -- Added explicit table reference and LIMIT 1 to prevent multiple-row errors + SELECT app_user.id + INTO v_super_user_id + FROM app_user + LEFT JOIN user_role ur ON app_user.id = ur.user_id + WHERE ur.role = 'SUPER_USER' + LIMIT 1; + + -- Explicitly check for NULL rather than evaluating the integer + IF v_super_user_id IS NOT NULL THEN + INSERT INTO modification_new(id, user_id, firearm_id, name, code, tags, note, author, + video_url, accessories, status, create_by, create_time) + SELECT m.id, + NULL, + m.firearm_id, + m.name, + m.code, + COALESCE(m.tags, '[]'::JSONB), + m.note, + m.author, + m.video_url, + COALESCE(m.accessories, '[]'::JSONB), + 2, + v_super_user_id, + now() + FROM modification m; + ELSE + -- Fixed RAISE NOTICE parameter formatting + RAISE NOTICE 'Super user not found. Please execute the manual SQL commands after adding a super user.'; + END IF; + END +$$; + +ALTER INDEX idx_modification_firearm_id RENAME TO idx_modification_lagacy_firearm_id; + +CREATE INDEX idx_modification_firearm_id ON modification_new (firearm_id); + +ALTER TABLE modification + RENAME TO modification_legacy; +ALTER TABLE modification_new + RENAME TO modification; + +COMMIT; \ No newline at end of file