2026-07-09 13:02:49 +08:00

427 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package countryregion
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/integration/roomclient"
"hyapp-admin-server/internal/integration/userclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/repository"
"hyapp-admin-server/internal/response"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type Handler struct {
cfg config.Config
service *Service
audit shared.OperationLogger
}
func New(userClient userclient.Client, userDB *sql.DB, jobStore *repository.Store, roomClient roomclient.Client, cfg config.Config, audit shared.OperationLogger) *Handler {
return &Handler{cfg: cfg, service: NewService(userClient, userDB, jobStore, roomClient), audit: audit}
}
func (h *Handler) Service() *Service {
if h == nil {
return nil
}
return h.service
}
// ListCountries 返回国家主数据列表enabled 不传时返回全部国家。
func (h *Handler) ListCountries(c *gin.Context) {
enabled, ok := parseOptionalBoolQuery(c, "enabled")
if !ok {
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
items, err := h.service.ListCountries(ctx, middleware.CurrentRequestID(c), enabled)
if err != nil {
writeCountryRegionError(c, err, "获取国家列表失败")
return
}
response.OK(c, gin.H{"items": items, "total": len(items)})
}
// GetCountry 返回单个国家user-service 没有单条 RPC 时由 service 复用列表做轻量过滤。
func (h *Handler) GetCountry(c *gin.Context) {
countryID, ok := parseInt64ID(c, "country_id")
if !ok {
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
country, err := h.service.GetCountry(ctx, middleware.CurrentRequestID(c), countryID)
if err != nil {
writeCountryRegionError(c, err, "获取国家失败")
return
}
response.OK(c, country)
}
// CreateCountry 创建国家这是后台管理后端能力countryCode 创建后不可变。
func (h *Handler) CreateCountry(c *gin.Context) {
var req createCountryRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "国家参数不正确")
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
country, err := h.service.CreateCountry(ctx, actorID(c), middleware.CurrentRequestID(c), req)
if err != nil {
writeCountryRegionError(c, err, "创建国家失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "create-country", "countries", country.CountryID,
fmt.Sprintf("country_id=%d country_code=%s enabled=%t", country.CountryID, country.CountryCode, country.Enabled))
response.Created(c, country)
}
// UpdateCountry 修改国家展示和辅助字段;不会转发 countryCode避免后台误改主键语义。
func (h *Handler) UpdateCountry(c *gin.Context) {
countryID, ok := parseInt64ID(c, "country_id")
if !ok {
return
}
var req updateCountryRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "国家参数不正确")
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
country, err := h.service.UpdateCountry(ctx, actorID(c), countryID, middleware.CurrentRequestID(c), req)
if err != nil {
writeCountryRegionError(c, err, "更新国家失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "update-country", "countries", country.CountryID,
fmt.Sprintf("country_id=%d country_code=%s", country.CountryID, country.CountryCode))
response.OK(c, country)
}
// RenameCountryCode 创建国家码重命名后台任务;实际改码由 admin_jobs runner 串行执行并同步下游当前投影。
func (h *Handler) RenameCountryCode(c *gin.Context) {
countryID, ok := parseInt64ID(c, "country_id")
if !ok {
return
}
var req renameCountryCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "国家参数不正确")
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
actor := shared.ActorFromContext(c)
job, err := h.service.RenameCountryCode(ctx, int64(actor.UserID), actor.Username, countryID, middleware.CurrentRequestID(c), req)
if err != nil {
writeCountryRegionError(c, err, "创建国家码重命名任务失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "rename-country-code", "countries", countryID,
fmt.Sprintf("country_id=%d old_country_code=%s new_country_code=%s job_id=%d", job.CountryID, job.OldCountryCode, job.NewCountryCode, job.JobID))
response.Created(c, job)
}
// EnableCountry 重新开放国家给 App 注册和用户改国家入口。
func (h *Handler) EnableCountry(c *gin.Context) {
countryID, ok := parseInt64ID(c, "country_id")
if !ok {
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
country, err := h.service.EnableCountry(ctx, actorID(c), countryID, middleware.CurrentRequestID(c))
if err != nil {
writeCountryRegionError(c, err, "启用国家失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "enable-country", "countries", country.CountryID,
fmt.Sprintf("country_id=%d country_code=%s", country.CountryID, country.CountryCode))
response.OK(c, country)
}
// DisableCountry 是国家“删除”的实际业务动作:软禁用 App 可选状态,不硬删历史主数据。
func (h *Handler) DisableCountry(c *gin.Context) {
countryID, ok := parseInt64ID(c, "country_id")
if !ok {
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
country, err := h.service.DisableCountry(ctx, actorID(c), countryID, middleware.CurrentRequestID(c))
if err != nil {
writeCountryRegionError(c, err, "禁用国家失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "disable-country", "countries", country.CountryID,
fmt.Sprintf("country_id=%d country_code=%s", country.CountryID, country.CountryCode))
response.OK(c, country)
}
// ListRegions 返回区域列表status 为空时返回全部active/disabled 由 user-service 校验。
func (h *Handler) ListRegions(c *gin.Context) {
ctx, cancel := h.userServiceContext(c)
defer cancel()
items, err := h.service.ListRegions(ctx, middleware.CurrentRequestID(c), c.Query("status"))
if err != nil {
writeCountryRegionError(c, err, "获取区域列表失败")
return
}
response.OK(c, gin.H{"items": items, "total": len(items)})
}
// GetRegion 返回单个区域及其当前 active 国家码列表。
func (h *Handler) GetRegion(c *gin.Context) {
regionID, ok := parseInt64ID(c, "region_id")
if !ok {
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
region, err := h.service.GetRegion(ctx, middleware.CurrentRequestID(c), regionID)
if err != nil {
writeCountryRegionError(c, err, "获取区域失败")
return
}
response.OK(c, region)
}
// CreateRegion 创建区域并设置首版国家归属。
func (h *Handler) CreateRegion(c *gin.Context) {
var req createRegionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "区域参数不正确")
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
region, err := h.service.CreateRegion(ctx, actorID(c), middleware.CurrentRequestID(c), req)
if err != nil {
writeCountryRegionError(c, err, "创建区域失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "create-region", "regions", region.RegionID,
fmt.Sprintf("region_id=%d region_code=%s countries=%d", region.RegionID, region.RegionCode, len(region.Countries)))
response.Created(c, region)
}
// UpdateRegion 修改区域展示字段,不修改 countries。
func (h *Handler) UpdateRegion(c *gin.Context) {
regionID, ok := parseInt64ID(c, "region_id")
if !ok {
return
}
var req updateRegionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "区域参数不正确")
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
region, err := h.service.UpdateRegion(ctx, actorID(c), regionID, middleware.CurrentRequestID(c), req)
if err != nil {
writeCountryRegionError(c, err, "更新区域失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "update-region", "regions", region.RegionID,
fmt.Sprintf("region_id=%d region_code=%s", region.RegionID, region.RegionCode))
response.OK(c, region)
}
// ReplaceRegionCountries 替换区域国家归属,并触发 user-service 的用户区域重算任务。
func (h *Handler) ReplaceRegionCountries(c *gin.Context) {
regionID, ok := parseInt64ID(c, "region_id")
if !ok {
return
}
var req replaceRegionCountriesRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "区域国家参数不正确")
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
region, err := h.service.ReplaceRegionCountries(ctx, actorID(c), regionID, middleware.CurrentRequestID(c), req)
if err != nil {
writeCountryRegionError(c, err, "更新区域国家失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "replace-region-countries", "regions", region.RegionID,
fmt.Sprintf("region_id=%d region_code=%s countries=%d", region.RegionID, region.RegionCode, len(region.Countries)))
response.OK(c, region)
}
// EnableRegion 重新启用区域主数据。
func (h *Handler) EnableRegion(c *gin.Context) {
regionID, ok := parseInt64ID(c, "region_id")
if !ok {
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
region, err := h.service.EnableRegion(ctx, actorID(c), regionID, middleware.CurrentRequestID(c))
if err != nil {
writeCountryRegionError(c, err, "启用区域失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "enable-region", "regions", region.RegionID,
fmt.Sprintf("region_id=%d region_code=%s", region.RegionID, region.RegionCode))
response.OK(c, region)
}
// DisableRegion 是区域“删除”的实际业务动作:停用区域并释放 active 国家归属。
func (h *Handler) DisableRegion(c *gin.Context) {
regionID, ok := parseInt64ID(c, "region_id")
if !ok {
return
}
ctx, cancel := h.userServiceContext(c)
defer cancel()
region, err := h.service.DisableRegion(ctx, actorID(c), regionID, middleware.CurrentRequestID(c))
if err != nil {
writeCountryRegionError(c, err, "停用区域失败")
return
}
writeCountryRegionAuditLog(c, h.audit, "disable-region", "regions", region.RegionID,
fmt.Sprintf("region_id=%d region_code=%s", region.RegionID, region.RegionCode))
response.OK(c, region)
}
func (h *Handler) userServiceContext(c *gin.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(c.Request.Context(), h.cfg.UserService.RequestTimeout)
}
func actorID(c *gin.Context) int64 {
return int64(shared.ActorFromContext(c).UserID)
}
func parseInt64ID(c *gin.Context, name string) (int64, bool) {
id, ok := shared.ParseID(c, name)
if !ok {
return 0, false
}
return int64(id), true
}
func parseOptionalBoolQuery(c *gin.Context, name string) (*bool, bool) {
raw := strings.TrimSpace(c.Query(name))
if raw == "" {
return nil, true
}
value, err := strconv.ParseBool(raw)
if err != nil {
response.BadRequest(c, name+" 参数不正确")
return nil, false
}
return &value, true
}
func writeCountryRegionError(c *gin.Context, err error, fallback string) {
if errors.Is(err, errCountryNotFound) {
response.NotFound(c, "国家不存在")
return
}
if errors.Is(err, errCountryInvalid) {
response.BadRequest(c, "国家参数不正确")
return
}
if errors.Is(err, errCountryConflict) {
response.BadRequest(c, "国家已存在")
return
}
if errors.Is(err, errCountryRenameJobConflict) {
response.BadRequest(c, "国家码重命名任务正在处理中")
return
}
if errors.Is(err, errRegionNotFound) {
response.NotFound(c, "区域不存在")
return
}
if errors.Is(err, errRegionInvalid) {
response.BadRequest(c, "区域参数不正确")
return
}
if errors.Is(err, errRegionConflict) {
response.BadRequest(c, "区域已存在")
return
}
if errors.Is(err, errRegionCountryMapping) {
response.BadRequest(c, "国家已归属其他启用区域")
return
}
if errors.Is(err, errGlobalRegionProtected) {
response.BadRequest(c, "GLOBAL 区域不能停用或配置国家")
return
}
if st, ok := status.FromError(err); ok {
switch st.Code() {
case codes.InvalidArgument, codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
response.BadRequest(c, grpcMessage(st, fallback))
case codes.NotFound:
response.NotFound(c, grpcMessage(st, fallback))
default:
response.ServerError(c, fallback)
}
return
}
response.ServerError(c, fallback)
}
func grpcMessage(st *status.Status, fallback string) string {
if message := strings.TrimSpace(st.Message()); message != "" {
return message
}
return fallback
}
func writeCountryRegionAuditLog(c *gin.Context, logger shared.OperationLogger, action string, resource string, resourceID int64, detail string) {
// 国家/区域的后台审计只写 admin-server 自己的 admin_operation_logs。
// user-service 仍只保存国家/区域主数据事实和其自身领域审计。
shared.OperationLogWithResourceID(c, logger, action, resource, fmt.Sprintf("%d", resourceID), "success", detail)
}