Add user account management methods and update user-related mappers

This commit is contained in:
zyh
2025-08-24 17:06:52 +08:00
parent bc1f10381a
commit 4cfd19195f
15 changed files with 430 additions and 3 deletions

View File

@@ -40,5 +40,9 @@ public class UserController {
.filter(Boolean::booleanValue)
.then();
}
}
@PutMapping("/{id}")
public Mono<User> update(@PathVariable Long id, @Valid @RequestBody User user) {
return userService.update(id, user);
}
}

View File

@@ -0,0 +1,66 @@
package com.gameplatform.server.controller.admin;
import com.gameplatform.server.model.dto.account.*;
import com.gameplatform.server.model.dto.common.PageResult;
import com.gameplatform.server.service.account.AccountService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/admin/accounts")
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@GetMapping
public Mono<PageResult<AccountResponse>> list(
@RequestParam(value = "userType", required = false) String userType,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "role", required = false) String role,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "size", required = false) Integer size
) {
return accountService.list(userType, status, role, keyword, page, size);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<AccountResponse> create(@Valid @RequestBody AccountCreateRequest req) {
return accountService.create(req);
}
@GetMapping("/{id}")
public Mono<AccountResponse> detail(@PathVariable Long id) {
return accountService.get(id);
}
@PatchMapping("/{id}")
public Mono<AccountResponse> update(@PathVariable Long id, @Valid @RequestBody AccountUpdateRequest req) {
return accountService.update(id, req);
}
@PostMapping("/{id}/enable")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> enable(@PathVariable Long id) {
return accountService.setStatus(id, "ENABLED").then();
}
@PostMapping("/{id}/disable")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> disable(@PathVariable Long id) {
return accountService.setStatus(id, "DISABLED").then();
}
@PostMapping("/{id}/reset-password")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> resetPassword(@PathVariable Long id, @Valid @RequestBody ResetPasswordRequest req) {
return accountService.resetPassword(id, req.getNewPassword(), Boolean.TRUE.equals(req.getForceLogout()));
}
}

View File

@@ -13,5 +13,6 @@ public interface UserMapper {
int insert(User user);
int deleteById(@Param("id") Long id);
}
int update(User user);
}

View File

@@ -7,4 +7,21 @@ public interface UserAccountMapper {
UserAccount findByUsernameAndType(@Param("username") String username,
@Param("userType") String userType);
UserAccount findByUsername(@Param("username") String username);
UserAccount findById(@Param("id") Long id);
int insert(UserAccount account);
int update(UserAccount account);
int setStatus(@Param("id") Long id, @Param("status") String status);
int updatePassword(@Param("id") Long id, @Param("passwordHash") String passwordHash);
long countByFilter(@Param("userType") String userType,
@Param("status") String status,
@Param("role") String role,
@Param("keyword") String keyword);
java.util.List<UserAccount> listByFilter(@Param("userType") String userType,
@Param("status") String status,
@Param("role") String role,
@Param("keyword") String keyword,
@Param("size") int size,
@Param("offset") int offset);
}

View File

@@ -0,0 +1,36 @@
package com.gameplatform.server.model.dto.account;
import jakarta.validation.constraints.*;
public class AccountCreateRequest {
@NotBlank
private String userType; // ADMIN | AGENT
@NotBlank
@Size(min = 3, max = 64)
private String username;
@Size(max = 100)
private String displayName;
private String role; // for ADMIN: SUPER | ADMIN
private String status = "ENABLED"; // ENABLED | DISABLED
@NotBlank
@Size(min = 6, max = 128)
private String initialPassword;
@Min(0)
private Long pointsBalance = 0L; // for AGENT
public String getUserType() { return userType; }
public void setUserType(String userType) { this.userType = userType; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getInitialPassword() { return initialPassword; }
public void setInitialPassword(String initialPassword) { this.initialPassword = initialPassword; }
public Long getPointsBalance() { return pointsBalance; }
public void setPointsBalance(Long pointsBalance) { this.pointsBalance = pointsBalance; }
}

View File

@@ -0,0 +1,35 @@
package com.gameplatform.server.model.dto.account;
import java.time.LocalDateTime;
public class AccountResponse {
private Long id;
private String userType;
private String username;
private String displayName;
private String role;
private String status;
private Long pointsBalance;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUserType() { return userType; }
public void setUserType(String userType) { this.userType = userType; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Long getPointsBalance() { return pointsBalance; }
public void setPointsBalance(Long pointsBalance) { this.pointsBalance = pointsBalance; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,18 @@
package com.gameplatform.server.model.dto.account;
import jakarta.validation.constraints.Size;
public class AccountUpdateRequest {
@Size(max = 100)
private String displayName;
private String role; // SUPER | ADMIN (only for ADMIN)
private String status; // ENABLED | DISABLED
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}

View File

@@ -0,0 +1,17 @@
package com.gameplatform.server.model.dto.account;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class ResetPasswordRequest {
@NotBlank
@Size(min = 6, max = 128)
private String newPassword;
private Boolean forceLogout = Boolean.TRUE;
public String getNewPassword() { return newPassword; }
public void setNewPassword(String newPassword) { this.newPassword = newPassword; }
public Boolean getForceLogout() { return forceLogout; }
public void setForceLogout(Boolean forceLogout) { this.forceLogout = forceLogout; }
}

View File

@@ -0,0 +1,29 @@
package com.gameplatform.server.model.dto.common;
import java.util.List;
public class PageResult<T> {
private List<T> items;
private long total;
private int page;
private int size;
public PageResult() {}
public PageResult(List<T> items, long total, int page, int size) {
this.items = items;
this.total = total;
this.page = page;
this.size = size;
}
public List<T> getItems() { return items; }
public void setItems(List<T> items) { this.items = items; }
public long getTotal() { return total; }
public void setTotal(long total) { this.total = total; }
public int getPage() { return page; }
public void setPage(int page) { this.page = page; }
public int getSize() { return size; }
public void setSize(int size) { this.size = size; }
}

View File

@@ -41,5 +41,14 @@ public class UserService {
return Mono.fromCallable(() -> userMapper.deleteById(id) > 0)
.subscribeOn(Schedulers.boundedElastic());
}
}
public Mono<User> update(Long id, User user) {
return Mono.fromCallable(() -> {
user.setId(id);
int n = userMapper.update(user);
return n;
})
.subscribeOn(Schedulers.boundedElastic())
.flatMap(n -> n > 0 ? getById(id) : Mono.empty());
}
}

View File

@@ -0,0 +1,131 @@
package com.gameplatform.server.service.account;
import com.gameplatform.server.mapper.account.UserAccountMapper;
import com.gameplatform.server.model.dto.account.AccountCreateRequest;
import com.gameplatform.server.model.dto.account.AccountResponse;
import com.gameplatform.server.model.dto.account.AccountUpdateRequest;
import com.gameplatform.server.model.dto.common.PageResult;
import com.gameplatform.server.model.entity.account.UserAccount;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class AccountService {
private final UserAccountMapper mapper;
private final PasswordEncoder passwordEncoder;
public AccountService(UserAccountMapper mapper, PasswordEncoder passwordEncoder) {
this.mapper = mapper;
this.passwordEncoder = passwordEncoder;
}
public Mono<PageResult<AccountResponse>> list(String userType, String status, String role, String keyword,
Integer page, Integer size) {
int p = (page == null || page < 1) ? 1 : page;
int s = (size == null || size < 1 || size > 200) ? 20 : size;
int offset = (p - 1) * s;
return Mono.fromCallable(() -> {
long total = mapper.countByFilter(userType, status, role, keyword);
List<UserAccount> list = mapper.listByFilter(userType, status, role, keyword, s, offset);
List<AccountResponse> items = list.stream().map(this::toResp).collect(Collectors.toList());
return new PageResult<>(items, total, p, s);
})
.subscribeOn(Schedulers.boundedElastic());
}
@Transactional
public Mono<AccountResponse> create(AccountCreateRequest req) {
return Mono.fromCallable(() -> {
// basic normalize
String type = req.getUserType().toUpperCase();
if (!type.equals("ADMIN") && !type.equals("AGENT")) {
throw new IllegalArgumentException("userType 只能是 ADMIN 或 AGENT");
}
if (mapper.findByUsername(req.getUsername()) != null) {
throw new IllegalArgumentException("用户名已存在");
}
UserAccount acc = new UserAccount();
acc.setUserType(type);
acc.setUsername(req.getUsername());
acc.setDisplayName(req.getDisplayName());
acc.setStatus(req.getStatus() == null ? "ENABLED" : req.getStatus());
if ("ADMIN".equals(type)) {
acc.setRole(req.getRole() == null ? "ADMIN" : req.getRole());
acc.setPointsBalance(0L);
} else {
acc.setRole(null);
acc.setPointsBalance(req.getPointsBalance() == null ? 0L : req.getPointsBalance());
}
acc.setPasswordHash(passwordEncoder.encode(req.getInitialPassword()));
mapper.insert(acc);
return toResp(acc);
})
.subscribeOn(Schedulers.boundedElastic());
}
public Mono<AccountResponse> get(Long id) {
return Mono.fromCallable(() -> mapper.findById(id))
.subscribeOn(Schedulers.boundedElastic())
.map(this::toResp);
}
@Transactional
public Mono<AccountResponse> update(Long id, AccountUpdateRequest req) {
return Mono.fromCallable(() -> {
UserAccount db = mapper.findById(id);
if (db == null) return null;
UserAccount patch = new UserAccount();
patch.setId(id);
patch.setDisplayName(req.getDisplayName());
// Only ADMIN account may set role; AGENT's role must remain null
if ("ADMIN".equalsIgnoreCase(db.getUserType())) {
patch.setRole(req.getRole());
}
patch.setStatus(req.getStatus());
mapper.update(patch);
return mapper.findById(id);
})
.subscribeOn(Schedulers.boundedElastic())
.map(this::toResp);
}
@Transactional
public Mono<Boolean> setStatus(Long id, String status) {
return Mono.fromCallable(() -> mapper.setStatus(id, status) > 0)
.subscribeOn(Schedulers.boundedElastic());
}
@Transactional
public Mono<Void> resetPassword(Long id, String newPassword, boolean forceLogout) {
return Mono.fromRunnable(() -> {
String hash = passwordEncoder.encode(newPassword);
mapper.updatePassword(id, hash);
// TODO: forceLogout 可结合 token 版本/黑名单实现(后续扩展)
})
.subscribeOn(Schedulers.boundedElastic())
.then();
}
private AccountResponse toResp(UserAccount a) {
if (a == null) return null;
AccountResponse r = new AccountResponse();
r.setId(a.getId());
r.setUserType(a.getUserType());
r.setUsername(a.getUsername());
r.setDisplayName(a.getDisplayName());
r.setRole(a.getRole());
r.setStatus(a.getStatus());
r.setPointsBalance(a.getPointsBalance());
r.setCreatedAt(a.getCreatedAt());
r.setUpdatedAt(a.getUpdatedAt());
return r;
}
}

View File

@@ -36,4 +36,11 @@
DELETE FROM users WHERE id = #{id}
</delete>
<update id="update" parameterType="com.gameplatform.server.model.User">
UPDATE users
SET username = #{username},
email = #{email}
WHERE id = #{id}
</update>
</mapper>

View File

@@ -28,4 +28,61 @@
WHERE username = #{username}
LIMIT 1
</select>
<select id="findById" parameterType="long" resultMap="UserAccountMap">
SELECT id, user_type, username, display_name, password_hash, role, status, points_balance, created_at, updated_at
FROM user_account
WHERE id = #{id}
LIMIT 1
</select>
<insert id="insert" parameterType="com.gameplatform.server.model.entity.account.UserAccount" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user_account (user_type, username, display_name, password_hash, role, status, points_balance)
VALUES (#{userType}, #{username}, #{displayName}, #{passwordHash}, #{role}, #{status}, #{pointsBalance})
</insert>
<update id="update" parameterType="com.gameplatform.server.model.entity.account.UserAccount">
UPDATE user_account
<set>
<if test="displayName != null">display_name = #{displayName},</if>
<if test="role != null">role = #{role},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<update id="setStatus">
UPDATE user_account SET status = #{status} WHERE id = #{id}
</update>
<update id="updatePassword">
UPDATE user_account SET password_hash = #{passwordHash} WHERE id = #{id}
</update>
<select id="countByFilter" resultType="long">
SELECT COUNT(1) FROM user_account
<where>
<if test="userType != null and userType != ''">AND user_type = #{userType}</if>
<if test="status != null and status != ''">AND status = #{status}</if>
<if test="role != null and role != ''">AND role = #{role}</if>
<if test="keyword != null and keyword != ''">
AND (username LIKE CONCAT('%', #{keyword}, '%') OR display_name LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
</select>
<select id="listByFilter" resultMap="UserAccountMap">
SELECT id, user_type, username, display_name, password_hash, role, status, points_balance, created_at, updated_at
FROM user_account
<where>
<if test="userType != null and userType != ''">AND user_type = #{userType}</if>
<if test="status != null and status != ''">AND status = #{status}</if>
<if test="role != null and role != ''">AND role = #{role}</if>
<if test="keyword != null and keyword != ''">
AND (username LIKE CONCAT('%', #{keyword}, '%') OR display_name LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
ORDER BY id DESC
LIMIT #{size} OFFSET #{offset}
</select>
</mapper>