387 lines
12 KiB
Go
387 lines
12 KiB
Go
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)
|
||
}
|