feat(mod): add status and creator tracking

- Introduce ModificationStatus enum with integer codes and a JPA converter
- Record the creating user on new modifications
- Rebuild the modification table with status, creator and audit columns
- Simplify user_role to a single-column primary key
This commit is contained in:
zihluwang committed 2026-08-20 15:18:49 +08:00
1 parent 4d9bf64ef4
commit 2936b6aece
11 files changed
+272 -146

No files matched your search

@@ -4,7 +4,9 @@ import com.onixbyte.deltaforceguide.domain.dto.ModificationBatchCreateRequest;
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest; import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse; 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.User;
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth; import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
import com.onixbyte.deltaforceguide.security.resolver.CurrentUser;
import com.onixbyte.deltaforceguide.service.ModificationService; import com.onixbyte.deltaforceguide.service.ModificationService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -66,15 +68,21 @@ public class ModificationController {
@RequiresAuth @RequiresAuth
@Operation(description = "创建改装") @Operation(description = "创建改装")
@PostMapping @PostMapping
public ModificationResponse create(@Valid @RequestBody ModificationRequest request) { public ModificationResponse create(
return modificationService.create(request); @CurrentUser User user,
@Valid @RequestBody ModificationRequest request
) {
return modificationService.create(request, user);
} }
@RequiresAuth @RequiresAuth
@Operation(description = "批量创建改装") @Operation(description = "批量创建改装")
@PostMapping("/batch") @PostMapping("/batch")
public List<ModificationResponse> batchCreate(@Valid @RequestBody ModificationBatchCreateRequest request) { public List<ModificationResponse> batchCreate(
return modificationService.batchCreate(request.modifications()); @CurrentUser User user,
@Valid @RequestBody ModificationBatchCreateRequest request
) {
return modificationService.batchCreate(request.modifications(), user);
} }
@RequiresAuth @RequiresAuth
@@ -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<ModificationStatus, Integer> {
@Override
public Integer convertToDatabaseColumn(ModificationStatus attribute) {
return attribute == null ? null : attribute.getCode();
}
@Override
public ModificationStatus convertToEntityAttribute(Integer dbData) {
return ModificationStatus.fromCode(dbData);
}
}
@@ -1,11 +1,15 @@
package com.onixbyte.deltaforceguide.domain.entity; 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 io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.*; import jakarta.persistence.*;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
import java.time.OffsetDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* Entity representing a firearm modification or build configuration. * Entity representing a firearm modification or build configuration.
@@ -25,6 +29,10 @@ public class Modification {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY, optional = false) @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "firearm_id", nullable = false, foreignKey = @ForeignKey(name = "fk_modification_firearm")) @JoinColumn(name = "firearm_id", nullable = false, foreignKey = @ForeignKey(name = "fk_modification_firearm"))
private Firearm firearm; private Firearm firearm;
@@ -36,11 +44,11 @@ public class Modification {
private String code; private String code;
@Type(JsonType.class) @Type(JsonType.class)
@Column(name = "tags", columnDefinition = "jsonb") @Column(name = "tags", columnDefinition = "jsonb", nullable = false)
private List<String> tags = new ArrayList<>(); private List<String> tags = new ArrayList<>();
@Type(JsonType.class) @Type(JsonType.class)
@Column(name = "accessories", columnDefinition = "jsonb") @Column(name = "accessories", columnDefinition = "jsonb", nullable = false)
private List<Accessory> accessories = new ArrayList<>(); private List<Accessory> accessories = new ArrayList<>();
@Column(name = "note", columnDefinition = "TEXT") @Column(name = "note", columnDefinition = "TEXT")
@@ -52,6 +60,16 @@ public class Modification {
@Column(name = "video_url", length = 512) @Column(name = "video_url", length = 512)
private String videoUrl; 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() { public Long getId() {
return id; return id;
} }
@@ -132,6 +150,38 @@ public class Modification {
this.videoUrl = videoUrl; 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() { public static Builder builder() {
return new Builder(); return new Builder();
} }
@@ -139,6 +189,7 @@ public class Modification {
public static class Builder { public static class Builder {
private Long id; private Long id;
private User user;
private Firearm firearm; private Firearm firearm;
private String name; private String name;
private String code; private String code;
@@ -147,12 +198,20 @@ public class Modification {
private String note; private String note;
private String author; private String author;
private String videoUrl; private String videoUrl;
private Long createBy;
private OffsetDateTime createTime;
private ModificationStatus status;
public Builder id(Long id) { public Builder id(Long id) {
this.id = id; this.id = id;
return this; return this;
} }
public Builder user(User user) {
this.user = user;
return this;
}
public Builder firearm(Firearm firearm) { public Builder firearm(Firearm firearm) {
this.firearm = firearm; this.firearm = firearm;
return this; return this;
@@ -193,17 +252,36 @@ public class Modification {
return this; 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() { public Modification build() {
Modification modification = new Modification(); Modification modification = new Modification();
modification.id = this.id; modification.id = this.id;
modification.user = this.user;
modification.firearm = this.firearm; modification.firearm = this.firearm;
modification.name = this.name; modification.name = this.name;
modification.code = this.code; modification.code = this.code;
modification.tags = this.tags == null ? new ArrayList<>() : this.tags; modification.tags = Optional.ofNullable(this.tags).orElseGet(ArrayList::new);
modification.accessories = this.accessories == null ? new ArrayList<>() : this.accessories; modification.accessories = Optional.ofNullable(this.accessories).orElseGet(ArrayList::new);
modification.note = this.note; modification.note = this.note;
modification.author = this.author; modification.author = this.author;
modification.videoUrl = this.videoUrl; 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; return modification;
} }
} }
@@ -1,12 +1,10 @@
package com.onixbyte.deltaforceguide.domain.entity; package com.onixbyte.deltaforceguide.domain.entity;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey; import jakarta.persistence.ForeignKey;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId; import jakarta.persistence.MapsId;
@@ -14,6 +12,8 @@ import jakarta.persistence.Table;
/** /**
* Entity representing a role assignment linking a user to a security role. * Entity representing a role assignment linking a user to a security role.
* <p>
* A user owns at most one role assignment, so the user ID is the primary key.
* *
* @author zihluwang * @author zihluwang
*/ */
@@ -21,24 +21,24 @@ import jakarta.persistence.Table;
@Table(name = "user_role") @Table(name = "user_role")
public class UserRole { public class UserRole {
@EmbeddedId @Id
@AttributeOverrides({ @Column(name = "user_id")
@AttributeOverride(name = "userId", column = @Column(name = "user_id")), private Long userId;
@AttributeOverride(name = "role", column = @Column(name = "role"))
})
private UserRoleId id = new UserRoleId();
@MapsId("userId") @MapsId("userId")
@ManyToOne(fetch = FetchType.LAZY, optional = false) @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_role_user")) @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_role_user"))
private User user; private User user;
public UserRoleId getId() { @Column(name = "role", nullable = false, length = 32)
return id; private String role;
public Long getUserId() {
return userId;
} }
public void setId(UserRoleId id) { public void setUserId(Long userId) {
this.id = id; this.userId = userId;
} }
public User getUser() { public User getUser() {
@@ -47,32 +47,17 @@ public class UserRole {
public void setUser(User user) { public void setUser(User user) {
this.user = user; this.user = user;
if (this.id == null) { if (user != null) {
this.id = new UserRoleId(); 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() { public String getRole() {
return id == null ? null : id.getRole(); return role;
} }
public void setRole(String role) { public void setRole(String role) {
if (this.id == null) { this.role = role;
this.id = new UserRoleId();
}
this.id.setRole(role);
} }
public static Builder builder() { public static Builder builder() {
@@ -81,16 +66,10 @@ public class UserRole {
public static class Builder { public static class Builder {
private UserRoleId id;
private User user; private User user;
private Long userId; private Long userId;
private String role; private String role;
public Builder id(UserRoleId id) {
this.id = id;
return this;
}
public Builder user(User user) { public Builder user(User user) {
this.user = user; this.user = user;
return this; return this;
@@ -108,16 +87,13 @@ public class UserRole {
public UserRole build() { public UserRole build() {
UserRole userRole = new UserRole(); UserRole userRole = new UserRole();
userRole.id = this.id == null ? new UserRoleId() : this.id;
userRole.user = this.user; userRole.user = this.user;
userRole.role = this.role;
if (this.user != null) { if (this.user != null) {
userRole.id.setUserId(this.user.getId()); userRole.userId = this.user.getId();
} }
if (this.userId != null) { if (this.userId != null) {
userRole.id.setUserId(this.userId); userRole.userId = this.userId;
}
if (this.role != null) {
userRole.id.setRole(this.role);
} }
return userRole; return userRole;
} }
@@ -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;
}
}
}
@@ -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.
* <p>
* 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<Integer, ModificationStatus> 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);
}
}
@@ -4,6 +4,7 @@ import com.onixbyte.deltaforceguide.domain.entity.Accessory;
import com.onixbyte.deltaforceguide.domain.entity.Firearm; import com.onixbyte.deltaforceguide.domain.entity.Firearm;
import com.onixbyte.deltaforceguide.domain.entity.Modification; import com.onixbyte.deltaforceguide.domain.entity.Modification;
import com.onixbyte.deltaforceguide.domain.entity.Tuning; 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.AccessoryRequest;
import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest; import com.onixbyte.deltaforceguide.domain.dto.ModificationRequest;
import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse; import com.onixbyte.deltaforceguide.domain.dto.ModificationResponse;
@@ -36,16 +37,16 @@ public class ModificationManager {
} }
@Transactional @Transactional
public ModificationResponse create(ModificationRequest request) { public ModificationResponse create(ModificationRequest request, User user) {
var firearm = firearmRepository.findById(request.firearmId()) var firearm = firearmRepository.findById(request.firearmId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Firearm not found: " + request.firearmId())); "Firearm not found: " + request.firearmId()));
var modification = toEntity(request, firearm); var modification = toEntity(request, firearm, user);
return ModificationResponse.from(modificationRepository.save(modification)); return ModificationResponse.from(modificationRepository.save(modification));
} }
@Transactional @Transactional
public List<ModificationResponse> batchCreate(List<ModificationRequest> requests) { public List<ModificationResponse> batchCreate(List<ModificationRequest> requests, User user) {
var firearmIds = requests.stream() var firearmIds = requests.stream()
.map(ModificationRequest::firearmId) .map(ModificationRequest::firearmId)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
@@ -63,7 +64,7 @@ public class ModificationManager {
} }
var modifications = requests.stream() var modifications = requests.stream()
.map(req -> toEntity(req, firearmMap.get(req.firearmId()))) .map(req -> toEntity(req, firearmMap.get(req.firearmId()), user))
.toList(); .toList();
return modificationRepository.saveAll(modifications) return modificationRepository.saveAll(modifications)
.stream() .stream()
@@ -86,7 +87,7 @@ public class ModificationManager {
return matches.getFirst().getId(); return matches.getFirst().getId();
} }
private Modification toEntity(ModificationRequest request, Firearm firearm) { private Modification toEntity(ModificationRequest request, Firearm firearm, User user) {
return Modification.builder() return Modification.builder()
.firearm(firearm) .firearm(firearm)
.name(request.name()) .name(request.name())
@@ -96,6 +97,7 @@ public class ModificationManager {
.note(request.note()) .note(request.note())
.author(request.author()) .author(request.author())
.videoUrl(request.videoUrl()) .videoUrl(request.videoUrl())
.createBy(user == null ? null : user.getId())
.build(); .build();
} }
@@ -1,7 +1,6 @@
package com.onixbyte.deltaforceguide.repository; package com.onixbyte.deltaforceguide.repository;
import com.onixbyte.deltaforceguide.domain.entity.UserRole; import com.onixbyte.deltaforceguide.domain.entity.UserRole;
import com.onixbyte.deltaforceguide.domain.entity.UserRoleId;
import org.springframework.data.jpa.repository.*; import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@@ -14,7 +13,7 @@ import java.util.List;
* @author zihluwang * @author zihluwang
*/ */
@Repository @Repository
public interface UserRoleRepository extends JpaRepository<UserRole, UserRoleId>, JpaSpecificationExecutor<UserRole> { public interface UserRoleRepository extends JpaRepository<UserRole, Long>, JpaSpecificationExecutor<UserRole> {
/** /**
* Find all roles assigned to a given user. * Find all roles assigned to a given user.
@@ -41,7 +40,7 @@ public interface UserRoleRepository extends JpaRepository<UserRole, UserRoleId>,
select count(ur) > 0 select count(ur) > 0
from UserRole ur from UserRole ur
where ur.user.id = :userId where ur.user.id = :userId
and ur.id.role = :role and ur.role = :role
""") """)
boolean existsByUserIdAndRole(@Param("userId") Long userId, @Param("role") String role); boolean existsByUserIdAndRole(@Param("userId") Long userId, @Param("role") String role);
@@ -55,7 +54,7 @@ public interface UserRoleRepository extends JpaRepository<UserRole, UserRoleId>,
@Query(""" @Query("""
delete from UserRole ur delete from UserRole ur
where ur.user.id = :userId where ur.user.id = :userId
and ur.id.role = :role and ur.role = :role
""") """)
void deleteByUserIdAndRole(@Param("userId") Long userId, @Param("role") String role); void deleteByUserIdAndRole(@Param("userId") Long userId, @Param("role") String role);
@@ -8,6 +8,7 @@ import com.onixbyte.deltaforceguide.domain.dto.TuningRequest;
import com.onixbyte.deltaforceguide.domain.entity.Accessory; import com.onixbyte.deltaforceguide.domain.entity.Accessory;
import com.onixbyte.deltaforceguide.domain.entity.Firearm; import com.onixbyte.deltaforceguide.domain.entity.Firearm;
import com.onixbyte.deltaforceguide.domain.entity.Modification; 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.domain.entity.Tuning;
import com.onixbyte.deltaforceguide.manager.ModificationManager; import com.onixbyte.deltaforceguide.manager.ModificationManager;
import com.onixbyte.deltaforceguide.repository.FirearmRepository; import com.onixbyte.deltaforceguide.repository.FirearmRepository;
@@ -104,20 +105,22 @@ public class ModificationService {
* Creates a new modification for a given firearm. * Creates a new modification for a given firearm.
* *
* @param request the modification creation request * @param request the modification creation request
* @param user the authenticated user creating the modification
* @return the created modification response * @return the created modification response
*/ */
public ModificationResponse create(ModificationRequest request) { public ModificationResponse create(ModificationRequest request, User user) {
return modificationManager.create(request); return modificationManager.create(request, user);
} }
/** /**
* Creates multiple modifications in a single batch operation. * Creates multiple modifications in a single batch operation.
* *
* @param requests list of modification creation requests * @param requests list of modification creation requests
* @param user the authenticated user creating the modifications
* @return list of created modification responses * @return list of created modification responses
*/ */
public List<ModificationResponse> batchCreate(List<ModificationRequest> requests) { public List<ModificationResponse> batchCreate(List<ModificationRequest> requests, User user) {
return modificationManager.batchCreate(requests); return modificationManager.batchCreate(requests, user);
} }
/** /**
@@ -88,7 +88,7 @@ public class WebhookService {
private void processSingle(Long issueNumber, Map<String, Object> data) { private void processSingle(Long issueNumber, Map<String, Object> data) {
var request = mapToRequest(data); var request = mapToRequest(data);
log.info("Creating modification from issue #{}: name={}", issueNumber, request.name()); log.info("Creating modification from issue #{}: name={}", issueNumber, request.name());
modificationManager.create(request); modificationManager.create(request, null);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -102,7 +102,7 @@ public class WebhookService {
.map(this::mapToRequest) .map(this::mapToRequest)
.toList(); .toList();
log.info("Batch creating {} modifications from issue #{}", requests.size(), issueNumber); log.info("Batch creating {} modifications from issue #{}", requests.size(), issueNumber);
modificationManager.batchCreate(requests); modificationManager.batchCreate(requests, null);
} }
private ModificationRequest mapToRequest(Map<String, Object> data) { private ModificationRequest mapToRequest(Map<String, Object> data) {
@@ -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;