package countryregion import ( "context" "database/sql" "errors" "fmt" "strconv" "strings" "hyapp-admin-server/internal/config" "hyapp-admin-server/internal/integration/userclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "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, cfg config.Config, audit shared.OperationLogger) *Handler { return &Handler{cfg: cfg, service: NewService(userClient, userDB), audit: audit} } // 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) } // 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, 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) }