diff --git a/api/src/main/java/com/databasir/api/Routes.java b/api/src/main/java/com/databasir/api/Routes.java index 4aaa5ef..34a817a 100644 --- a/api/src/main/java/com/databasir/api/Routes.java +++ b/api/src/main/java/com/databasir/api/Routes.java @@ -10,6 +10,8 @@ public interface Routes { String GET_ONE = BASE + "/users/{userId}"; + String DELETE_ONE = BASE + "/users/{userId}"; + String ENABLE = BASE + "/users/{userId}/enable"; String DISABLE = BASE + "/users/{userId}/disable"; diff --git a/api/src/main/java/com/databasir/api/UserController.java b/api/src/main/java/com/databasir/api/UserController.java index f464781..09d540c 100644 --- a/api/src/main/java/com/databasir/api/UserController.java +++ b/api/src/main/java/com/databasir/api/UserController.java @@ -8,7 +8,6 @@ import com.databasir.core.domain.DomainErrors; import com.databasir.core.domain.log.annotation.Operation; import com.databasir.core.domain.user.data.*; import com.databasir.core.domain.user.service.UserService; -import com.databasir.core.infrastructure.event.EventPublisher; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -29,8 +28,6 @@ public class UserController { private final UserOperationValidator userOperationValidator; - private final EventPublisher eventPublisher; - @GetMapping(Routes.User.LIST) public JsonData> list(@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable, @@ -73,6 +70,16 @@ public class UserController { return JsonData.ok(userService.get(userId)); } + @DeleteMapping(Routes.User.DELETE_ONE) + @PreAuthorize("hasAnyAuthority('SYS_OWNER')") + public JsonData deleteOne(@PathVariable Integer userId) { + if (userOperationValidator.isMyself(userId)) { + throw DomainErrors.CANNOT_DELETE_SELF.exception(); + } + userService.deleteOne(userId); + return JsonData.ok(); + } + @PostMapping(Routes.User.RENEW_PASSWORD) @PreAuthorize("hasAnyAuthority('SYS_OWNER')") @Operation(module = Operation.Modules.USER, name = "重置用户密码", involvedUserId = "#userId") diff --git a/core/src/main/java/com/databasir/core/domain/DomainErrors.java b/core/src/main/java/com/databasir/core/domain/DomainErrors.java index 1ce7c88..143780c 100644 --- a/core/src/main/java/com/databasir/core/domain/DomainErrors.java +++ b/core/src/main/java/com/databasir/core/domain/DomainErrors.java @@ -42,6 +42,7 @@ public enum DomainErrors implements DatabasirErrors { CIRCLE_REFERENCE("A_10027", "检查到循环引用"), DUPLICATE_COLUMN("A_10028", "重复的列"), INVALID_MOCK_DATA_SCRIPT("A_10029", "不合法的表达式"), + CANNOT_DELETE_SELF("A_10030", "无法对自己执行删除账号操作"), ; private final String errCode; diff --git a/core/src/main/java/com/databasir/core/domain/user/service/UserService.java b/core/src/main/java/com/databasir/core/domain/user/service/UserService.java index 6c89828..478c88b 100644 --- a/core/src/main/java/com/databasir/core/domain/user/service/UserService.java +++ b/core/src/main/java/com/databasir/core/domain/user/service/UserService.java @@ -176,4 +176,12 @@ public class UserService { userPojo.setNickname(request.getNickname()); userDao.updateById(userPojo); } + + @Transactional + public void deleteOne(Integer userId) { + if (userDao.existsById(userId)) { + userDao.deleteById(userId); + loginDao.deleteByUserId(userId); + } + } } diff --git a/dao/generated-src/jooq/main/java/com/databasir/dao/Keys.java b/dao/generated-src/jooq/main/java/com/databasir/dao/Keys.java index d4c61ea..21f9e62 100644 --- a/dao/generated-src/jooq/main/java/com/databasir/dao/Keys.java +++ b/dao/generated-src/jooq/main/java/com/databasir/dao/Keys.java @@ -101,8 +101,8 @@ public class Keys { public static final UniqueKey KEY_TABLE_INDEX_DOCUMENT_PRIMARY = Internal.createUniqueKey(TableIndexDocument.TABLE_INDEX_DOCUMENT, DSL.name("KEY_table_index_document_PRIMARY"), new TableField[] { TableIndexDocument.TABLE_INDEX_DOCUMENT.ID }, true); public static final UniqueKey KEY_TABLE_TRIGGER_DOCUMENT_PRIMARY = Internal.createUniqueKey(TableTriggerDocument.TABLE_TRIGGER_DOCUMENT, DSL.name("KEY_table_trigger_document_PRIMARY"), new TableField[] { TableTriggerDocument.TABLE_TRIGGER_DOCUMENT.ID }, true); public static final UniqueKey KEY_USER_PRIMARY = Internal.createUniqueKey(User.USER, DSL.name("KEY_user_PRIMARY"), new TableField[] { User.USER.ID }, true); - public static final UniqueKey KEY_USER_UK_EMAIL = Internal.createUniqueKey(User.USER, DSL.name("KEY_user_uk_email"), new TableField[] { User.USER.EMAIL }, true); - public static final UniqueKey KEY_USER_UK_USERNAME = Internal.createUniqueKey(User.USER, DSL.name("KEY_user_uk_username"), new TableField[] { User.USER.USERNAME }, true); + public static final UniqueKey KEY_USER_UK_EMAIL = Internal.createUniqueKey(User.USER, DSL.name("KEY_user_uk_email"), new TableField[] { User.USER.EMAIL, User.USER.DELETED_TOKEN }, true); + public static final UniqueKey KEY_USER_UK_USERNAME = Internal.createUniqueKey(User.USER, DSL.name("KEY_user_uk_username"), new TableField[] { User.USER.USERNAME, User.USER.DELETED_TOKEN }, true); public static final UniqueKey KEY_USER_FAVORITE_PROJECT_PRIMARY = Internal.createUniqueKey(UserFavoriteProject.USER_FAVORITE_PROJECT, DSL.name("KEY_user_favorite_project_PRIMARY"), new TableField[] { UserFavoriteProject.USER_FAVORITE_PROJECT.ID }, true); public static final UniqueKey KEY_USER_FAVORITE_PROJECT_UK_USER_ID_PROJECT_ID = Internal.createUniqueKey(UserFavoriteProject.USER_FAVORITE_PROJECT, DSL.name("KEY_user_favorite_project_uk_user_id_project_id"), new TableField[] { UserFavoriteProject.USER_FAVORITE_PROJECT.USER_ID, UserFavoriteProject.USER_FAVORITE_PROJECT.PROJECT_ID }, true); public static final UniqueKey KEY_USER_ROLE_PRIMARY = Internal.createUniqueKey(UserRole.USER_ROLE, DSL.name("KEY_user_role_PRIMARY"), new TableField[] { UserRole.USER_ROLE.ID }, true); diff --git a/dao/generated-src/jooq/main/java/com/databasir/dao/tables/User.java b/dao/generated-src/jooq/main/java/com/databasir/dao/tables/User.java index 91ed384..eb6ef78 100644 --- a/dao/generated-src/jooq/main/java/com/databasir/dao/tables/User.java +++ b/dao/generated-src/jooq/main/java/com/databasir/dao/tables/User.java @@ -17,7 +17,7 @@ import org.jooq.ForeignKey; import org.jooq.Identity; import org.jooq.Name; import org.jooq.Record; -import org.jooq.Row9; +import org.jooq.Row11; import org.jooq.Schema; import org.jooq.Table; import org.jooq.TableField; @@ -84,6 +84,16 @@ public class User extends TableImpl { */ public final TableField ENABLED = createField(DSL.name("enabled"), SQLDataType.BOOLEAN.nullable(false).defaultValue(DSL.inline("0", SQLDataType.BOOLEAN)), this, ""); + /** + * The column databasir.user.deleted. + */ + public final TableField DELETED = createField(DSL.name("deleted"), SQLDataType.BOOLEAN.nullable(false).defaultValue(DSL.inline("0", SQLDataType.BOOLEAN)), this, ""); + + /** + * The column databasir.user.deleted_token. + */ + public final TableField DELETED_TOKEN = createField(DSL.name("deleted_token"), SQLDataType.INTEGER.nullable(false).defaultValue(DSL.inline("0", SQLDataType.INTEGER)), this, ""); + /** * The column databasir.user.update_at. */ @@ -174,11 +184,11 @@ public class User extends TableImpl { } // ------------------------------------------------------------------------- - // Row9 type methods + // Row11 type methods // ------------------------------------------------------------------------- @Override - public Row9 fieldsRow() { - return (Row9) super.fieldsRow(); + public Row11 fieldsRow() { + return (Row11) super.fieldsRow(); } } diff --git a/dao/generated-src/jooq/main/java/com/databasir/dao/tables/pojos/UserPojo.java b/dao/generated-src/jooq/main/java/com/databasir/dao/tables/pojos/UserPojo.java index 779d141..27870bd 100644 --- a/dao/generated-src/jooq/main/java/com/databasir/dao/tables/pojos/UserPojo.java +++ b/dao/generated-src/jooq/main/java/com/databasir/dao/tables/pojos/UserPojo.java @@ -23,6 +23,8 @@ public class UserPojo implements Serializable { private String nickname; private String avatar; private Boolean enabled; + private Boolean deleted; + private Integer deletedToken; private LocalDateTime updateAt; private LocalDateTime createAt; @@ -36,6 +38,8 @@ public class UserPojo implements Serializable { this.nickname = value.nickname; this.avatar = value.avatar; this.enabled = value.enabled; + this.deleted = value.deleted; + this.deletedToken = value.deletedToken; this.updateAt = value.updateAt; this.createAt = value.createAt; } @@ -48,6 +52,8 @@ public class UserPojo implements Serializable { String nickname, String avatar, Boolean enabled, + Boolean deleted, + Integer deletedToken, LocalDateTime updateAt, LocalDateTime createAt ) { @@ -58,6 +64,8 @@ public class UserPojo implements Serializable { this.nickname = nickname; this.avatar = avatar; this.enabled = enabled; + this.deleted = deleted; + this.deletedToken = deletedToken; this.updateAt = updateAt; this.createAt = createAt; } @@ -160,6 +168,34 @@ public class UserPojo implements Serializable { this.enabled = enabled; } + /** + * Getter for databasir.user.deleted. + */ + public Boolean getDeleted() { + return this.deleted; + } + + /** + * Setter for databasir.user.deleted. + */ + public void setDeleted(Boolean deleted) { + this.deleted = deleted; + } + + /** + * Getter for databasir.user.deleted_token. + */ + public Integer getDeletedToken() { + return this.deletedToken; + } + + /** + * Setter for databasir.user.deleted_token. + */ + public void setDeletedToken(Integer deletedToken) { + this.deletedToken = deletedToken; + } + /** * Getter for databasir.user.update_at. */ @@ -199,6 +235,8 @@ public class UserPojo implements Serializable { sb.append(", ").append(nickname); sb.append(", ").append(avatar); sb.append(", ").append(enabled); + sb.append(", ").append(deleted); + sb.append(", ").append(deletedToken); sb.append(", ").append(updateAt); sb.append(", ").append(createAt); diff --git a/dao/generated-src/jooq/main/java/com/databasir/dao/tables/records/UserRecord.java b/dao/generated-src/jooq/main/java/com/databasir/dao/tables/records/UserRecord.java index b37b51f..c362be1 100644 --- a/dao/generated-src/jooq/main/java/com/databasir/dao/tables/records/UserRecord.java +++ b/dao/generated-src/jooq/main/java/com/databasir/dao/tables/records/UserRecord.java @@ -11,8 +11,8 @@ import java.time.LocalDateTime; import org.jooq.Field; import org.jooq.Record1; -import org.jooq.Record9; -import org.jooq.Row9; +import org.jooq.Record11; +import org.jooq.Row11; import org.jooq.impl.UpdatableRecordImpl; @@ -20,7 +20,7 @@ import org.jooq.impl.UpdatableRecordImpl; * This class is generated by jOOQ. */ @SuppressWarnings({ "all", "unchecked", "rawtypes" }) -public class UserRecord extends UpdatableRecordImpl implements Record9 { +public class UserRecord extends UpdatableRecordImpl implements Record11 { private static final long serialVersionUID = 1L; @@ -122,32 +122,60 @@ public class UserRecord extends UpdatableRecordImpl implements Recor return (Boolean) get(6); } + /** + * Setter for databasir.user.deleted. + */ + public void setDeleted(Boolean value) { + set(7, value); + } + + /** + * Getter for databasir.user.deleted. + */ + public Boolean getDeleted() { + return (Boolean) get(7); + } + + /** + * Setter for databasir.user.deleted_token. + */ + public void setDeletedToken(Integer value) { + set(8, value); + } + + /** + * Getter for databasir.user.deleted_token. + */ + public Integer getDeletedToken() { + return (Integer) get(8); + } + /** * Setter for databasir.user.update_at. */ public void setUpdateAt(LocalDateTime value) { - set(7, value); + set(9, value); } /** * Getter for databasir.user.update_at. */ public LocalDateTime getUpdateAt() { - return (LocalDateTime) get(7); + return (LocalDateTime) get(9); } /** * Setter for databasir.user.create_at. */ public void setCreateAt(LocalDateTime value) { - set(8, value); + set(10, value); } /** * Getter for databasir.user.create_at. */ public LocalDateTime getCreateAt() { - return (LocalDateTime) get(8); + return (LocalDateTime) get(10); } // ------------------------------------------------------------------------- @@ -160,17 +188,17 @@ public class UserRecord extends UpdatableRecordImpl implements Recor } // ------------------------------------------------------------------------- - // Record9 type implementation + // Record11 type implementation // ------------------------------------------------------------------------- @Override - public Row9 fieldsRow() { - return (Row9) super.fieldsRow(); + public Row11 fieldsRow() { + return (Row11) super.fieldsRow(); } @Override - public Row9 valuesRow() { - return (Row9) super.valuesRow(); + public Row11 valuesRow() { + return (Row11) super.valuesRow(); } @Override @@ -209,12 +237,22 @@ public class UserRecord extends UpdatableRecordImpl implements Recor } @Override - public Field field8() { + public Field field8() { + return User.USER.DELETED; + } + + @Override + public Field field9() { + return User.USER.DELETED_TOKEN; + } + + @Override + public Field field10() { return User.USER.UPDATE_AT; } @Override - public Field field9() { + public Field field11() { return User.USER.CREATE_AT; } @@ -254,12 +292,22 @@ public class UserRecord extends UpdatableRecordImpl implements Recor } @Override - public LocalDateTime component8() { + public Boolean component8() { + return getDeleted(); + } + + @Override + public Integer component9() { + return getDeletedToken(); + } + + @Override + public LocalDateTime component10() { return getUpdateAt(); } @Override - public LocalDateTime component9() { + public LocalDateTime component11() { return getCreateAt(); } @@ -299,12 +347,22 @@ public class UserRecord extends UpdatableRecordImpl implements Recor } @Override - public LocalDateTime value8() { + public Boolean value8() { + return getDeleted(); + } + + @Override + public Integer value9() { + return getDeletedToken(); + } + + @Override + public LocalDateTime value10() { return getUpdateAt(); } @Override - public LocalDateTime value9() { + public LocalDateTime value11() { return getCreateAt(); } @@ -351,19 +409,31 @@ public class UserRecord extends UpdatableRecordImpl implements Recor } @Override - public UserRecord value8(LocalDateTime value) { + public UserRecord value8(Boolean value) { + setDeleted(value); + return this; + } + + @Override + public UserRecord value9(Integer value) { + setDeletedToken(value); + return this; + } + + @Override + public UserRecord value10(LocalDateTime value) { setUpdateAt(value); return this; } @Override - public UserRecord value9(LocalDateTime value) { + public UserRecord value11(LocalDateTime value) { setCreateAt(value); return this; } @Override - public UserRecord values(Integer value1, String value2, String value3, String value4, String value5, String value6, Boolean value7, LocalDateTime value8, LocalDateTime value9) { + public UserRecord values(Integer value1, String value2, String value3, String value4, String value5, String value6, Boolean value7, Boolean value8, Integer value9, LocalDateTime value10, LocalDateTime value11) { value1(value1); value2(value2); value3(value3); @@ -373,6 +443,8 @@ public class UserRecord extends UpdatableRecordImpl implements Recor value7(value7); value8(value8); value9(value9); + value10(value10); + value11(value11); return this; } @@ -390,7 +462,7 @@ public class UserRecord extends UpdatableRecordImpl implements Recor /** * Create a detached, initialised UserRecord */ - public UserRecord(Integer id, String email, String username, String password, String nickname, String avatar, Boolean enabled, LocalDateTime updateAt, LocalDateTime createAt) { + public UserRecord(Integer id, String email, String username, String password, String nickname, String avatar, Boolean enabled, Boolean deleted, Integer deletedToken, LocalDateTime updateAt, LocalDateTime createAt) { super(User.USER); setId(id); @@ -400,6 +472,8 @@ public class UserRecord extends UpdatableRecordImpl implements Recor setNickname(nickname); setAvatar(avatar); setEnabled(enabled); + setDeleted(deleted); + setDeletedToken(deletedToken); setUpdateAt(updateAt); setCreateAt(createAt); } @@ -418,6 +492,8 @@ public class UserRecord extends UpdatableRecordImpl implements Recor setNickname(value.getNickname()); setAvatar(value.getAvatar()); setEnabled(value.getEnabled()); + setDeleted(value.getDeleted()); + setDeletedToken(value.getDeletedToken()); setUpdateAt(value.getUpdateAt()); setCreateAt(value.getCreateAt()); } diff --git a/dao/src/main/java/com/databasir/dao/impl/UserDao.java b/dao/src/main/java/com/databasir/dao/impl/UserDao.java index cd8426b..d8368a4 100644 --- a/dao/src/main/java/com/databasir/dao/impl/UserDao.java +++ b/dao/src/main/java/com/databasir/dao/impl/UserDao.java @@ -15,6 +15,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Repository; +import java.io.Serializable; import java.util.*; import static com.databasir.dao.Tables.USER; @@ -31,15 +32,44 @@ public class UserDao extends BaseDao { super(USER, UserPojo.class); } + @Override + public int deleteById(T id) { + return getDslContext() + .deleteFrom(USER).where(identity().eq(id).and(USER.DELETED.eq(false))) + .execute(); + } + + @Override + public int delete(Condition condition) { + return getDslContext() + .update(USER) + .set(USER.DELETED, true) + .set(USER.DELETED_TOKEN, USER.ID) + .where(USER.DELETED.eq(false)) + .execute(); + } + + @Override + public boolean exists(Condition condition) { + return super.exists(condition.and(USER.DELETED.eq(false))); + } + + @Override + public boolean existsById(T id) { + return getDslContext().fetchExists(USER, identity().eq(id).and(USER.DELETED.eq(false))); + } + public void updateEnabledByUserId(Integer userId, Boolean enabled) { dslContext - .update(USER).set(USER.ENABLED, enabled).where(USER.ID.eq(userId)) + .update(USER).set(USER.ENABLED, enabled) + .where(USER.ID.eq(userId).and(USER.DELETED.eq(false))) .execute(); } public void updatePassword(Integer userId, String password) { dslContext - .update(USER).set(USER.PASSWORD, password).where(USER.ID.eq(userId)) + .update(USER).set(USER.PASSWORD, password) + .where(USER.ID.eq(userId).and(USER.DELETED.eq(false))) .execute(); } @@ -49,7 +79,7 @@ public class UserDao extends BaseDao { } return dslContext .select(USER.fields()).from(USER) - .where(USER.ID.in(userIds)) + .where(USER.ID.in(userIds).and(USER.DELETED.eq(false))) .fetchInto(UserPojo.class); } @@ -59,7 +89,8 @@ public class UserDao extends BaseDao { return dslContext .select(USER.fields()).from(USER) .innerJoin(USER_ROLE).on(USER_ROLE.USER_ID.eq(USER.ID)) - .where(USER_ROLE.GROUP_ID.eq(groupId).and(USER_ROLE.ROLE.eq(role))) + .where(USER.DELETED.eq(false) + .and(USER_ROLE.GROUP_ID.eq(groupId).and(USER_ROLE.ROLE.eq(role)))) .orderBy(USER_ROLE.ID.desc()) .limit(size) .fetchInto(UserPojo.class); @@ -67,21 +98,24 @@ public class UserDao extends BaseDao { public Optional selectByEmail(String email) { return dslContext - .select(USER.fields()).from(USER).where(USER.EMAIL.eq(email)) + .select(USER.fields()).from(USER) + .where(USER.EMAIL.eq(email).and(USER.DELETED.eq(false))) .fetchOptionalInto(UserPojo.class); } public Optional selectByEmailOrUsername(String emailOrUsername) { return dslContext .select(USER.fields()).from(USER) - .where(USER.EMAIL.eq(emailOrUsername).or(USER.USERNAME.eq(emailOrUsername))) + .where(USER.DELETED.eq(false) + .and(USER.EMAIL.eq(emailOrUsername).or(USER.USERNAME.eq(emailOrUsername)))) .fetchOptionalInto(UserPojo.class); } public List selectEnabledGroupMembers(Integer groupId) { return dslContext.select(USER.fields()).from(USER) .innerJoin(USER_ROLE).on(USER.ID.eq(USER_ROLE.USER_ID)) - .where(USER_ROLE.GROUP_ID.eq(groupId).and(USER.ENABLED.eq(true))) + .where(USER.DELETED.eq(false) + .and(USER_ROLE.GROUP_ID.eq(groupId).and(USER.ENABLED.eq(true)))) .fetchInto(UserPojo.class); } @@ -91,7 +125,8 @@ public class UserDao extends BaseDao { .selectCount() .from(USER) .innerJoin(USER_ROLE).on(USER.ID.eq(USER_ROLE.USER_ID)) - .where(USER_ROLE.GROUP_ID.eq(groupId).and(condition)) + .where(USER.DELETED.eq(false) + .and(USER_ROLE.GROUP_ID.eq(groupId).and(condition))) .fetchOne(0, int.class); // data @@ -100,7 +135,8 @@ public class UserDao extends BaseDao { USER_ROLE.USER_ID, USER_ROLE.ROLE, USER_ROLE.GROUP_ID, USER_ROLE.CREATE_AT) .from(USER) .innerJoin(USER_ROLE).on(USER.ID.eq(USER_ROLE.USER_ID)) - .where(USER_ROLE.GROUP_ID.eq(groupId).and(condition)) + .where(USER.DELETED.eq(false) + .and(USER_ROLE.GROUP_ID.eq(groupId).and(condition))) .orderBy(getSortFields(request.getSort())) .offset(request.getOffset()).limit(request.getPageSize()) .fetchInto(GroupMemberDetailPojo.class); diff --git a/dao/src/main/resources/db/migration/V1__init.sql b/dao/src/main/resources/db/migration/V1__init.sql index 9bca2a7..be8038c 100644 --- a/dao/src/main/resources/db/migration/V1__init.sql +++ b/dao/src/main/resources/db/migration/V1__init.sql @@ -27,17 +27,19 @@ CREATE TABLE IF NOT EXISTS sys_mail CREATE TABLE IF NOT EXISTS user ( - id INT PRIMARY KEY AUTO_INCREMENT, - email VARCHAR(512) NOT NULL, - username VARCHAR(128) NOT NULL, - password TEXT NOT NULL, - nickname VARCHAR(255) NOT NULL, - avatar VARCHAR(512) DEFAULT NULL, - enabled BOOLEAN NOT NULL DEFAULT FALSE, - update_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - create_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT UNIQUE uk_email (email), - CONSTRAINT UNIQUE uk_username (username) + id INT PRIMARY KEY AUTO_INCREMENT, + email VARCHAR(512) NOT NULL, + username VARCHAR(128) NOT NULL, + password TEXT NOT NULL, + nickname VARCHAR(255) NOT NULL, + avatar VARCHAR(512) DEFAULT NULL, + enabled BOOLEAN NOT NULL DEFAULT FALSE, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + deleted_token INT NOT NULL DEFAULT 0, + update_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + create_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT UNIQUE uk_email (email, deleted_token), + CONSTRAINT UNIQUE uk_username (username, deleted_token) ) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;