2026-05-29 19:39:41 +08:00
package app
import (
"context"
"fmt"
"net/http"
"strings"
"sub2api-cn-relay-manager/internal/store/sqlite"
)
type PortalLogicalGroupInfo struct {
LogicalGroupID string ` json:"logical_group_id" `
DisplayName string ` json:"display_name" `
Description string ` json:"description,omitempty" `
2026-05-30 10:38:59 +08:00
UsageScenario string ` json:"usage_scenario,omitempty" `
Recommendation string ` json:"recommendation,omitempty" `
NextStepHint string ` json:"next_step_hint,omitempty" `
2026-05-30 10:54:32 +08:00
VisibilityScope string ` json:"visibility_scope,omitempty" `
PackageTier string ` json:"package_tier,omitempty" `
PurchaseCTALabel string ` json:"purchase_cta_label,omitempty" `
PurchaseCTAURL string ` json:"purchase_cta_url,omitempty" `
2026-05-29 19:39:41 +08:00
Status string ` json:"status" `
StickyMode string ` json:"sticky_mode,omitempty" `
RoutePolicy string ` json:"route_policy,omitempty" `
PublicModels [ ] PortalLogicalGroupModel ` json:"public_models,omitempty" `
RouteCount int ` json:"route_count" `
ActiveRouteCount int ` json:"active_route_count" `
CreatedAt string ` json:"created_at,omitempty" `
UpdatedAt string ` json:"updated_at,omitempty" `
}
type PortalLogicalGroupModel struct {
PublicModel string ` json:"public_model" `
Status string ` json:"status,omitempty" `
}
func handleListPortalLogicalGroups ( w http . ResponseWriter , r * http . Request , fn func ( context . Context ) ( [ ] PortalLogicalGroupInfo , error ) ) {
if fn == nil {
writeHTTPError ( w , & httpError { StatusCode : http . StatusInternalServerError , Code : "server_misconfigured" , Message : "list-portal-logical-groups action is not configured" } )
return
}
groups , err := fn ( r . Context ( ) )
if err != nil {
writeHTTPError ( w , classifyError ( err ) )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "logical_groups" : groups } )
}
func handleGetPortalLogicalGroup ( w http . ResponseWriter , r * http . Request , fn func ( context . Context , string ) ( PortalLogicalGroupInfo , error ) ) {
if fn == nil {
writeHTTPError ( w , & httpError { StatusCode : http . StatusInternalServerError , Code : "server_misconfigured" , Message : "get-portal-logical-group action is not configured" } )
return
}
group , err := fn ( r . Context ( ) , strings . TrimSpace ( r . PathValue ( "groupID" ) ) )
if err != nil {
writeHTTPError ( w , classifyError ( err ) )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "logical_group" : group } )
}
func handleListPortalLogicalGroupModels ( w http . ResponseWriter , r * http . Request , fn func ( context . Context , string ) ( [ ] PortalLogicalGroupModel , error ) ) {
if fn == nil {
writeHTTPError ( w , & httpError { StatusCode : http . StatusInternalServerError , Code : "server_misconfigured" , Message : "list-portal-logical-group-models action is not configured" } )
return
}
models , err := fn ( r . Context ( ) , strings . TrimSpace ( r . PathValue ( "groupID" ) ) )
if err != nil {
writeHTTPError ( w , classifyError ( err ) )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "models" : models } )
}
func buildListPortalLogicalGroupsAction ( sqliteDSN string ) func ( context . Context ) ( [ ] PortalLogicalGroupInfo , error ) {
return func ( ctx context . Context ) ( [ ] PortalLogicalGroupInfo , error ) {
store , err := sqlite . Open ( ctx , sqliteDSN )
if err != nil {
return nil , err
}
defer store . Close ( )
rows , err := store . LogicalGroups ( ) . List ( ctx )
if err != nil {
return nil , err
}
items := make ( [ ] PortalLogicalGroupInfo , 0 , len ( rows ) )
for _ , row := range rows {
if ! isPortalLogicalGroupVisible ( row . Status ) {
continue
}
item , buildErr := buildPortalLogicalGroupInfo ( ctx , store , row )
if buildErr != nil {
return nil , buildErr
}
items = append ( items , item )
}
return items , nil
}
}
func buildGetPortalLogicalGroupAction ( sqliteDSN string ) func ( context . Context , string ) ( PortalLogicalGroupInfo , error ) {
return func ( ctx context . Context , logicalGroupID string ) ( PortalLogicalGroupInfo , error ) {
store , err := sqlite . Open ( ctx , sqliteDSN )
if err != nil {
return PortalLogicalGroupInfo { } , err
}
defer store . Close ( )
group , err := getLogicalGroupRow ( ctx , store , logicalGroupID )
if err != nil {
return PortalLogicalGroupInfo { } , err
}
if ! isPortalLogicalGroupVisible ( group . Status ) {
return PortalLogicalGroupInfo { } , fmt . Errorf ( "logical group %q not found" , strings . TrimSpace ( logicalGroupID ) )
}
return buildPortalLogicalGroupInfo ( ctx , store , group )
}
}
func buildListPortalLogicalGroupModelsAction ( sqliteDSN string ) func ( context . Context , string ) ( [ ] PortalLogicalGroupModel , error ) {
return func ( ctx context . Context , logicalGroupID string ) ( [ ] PortalLogicalGroupModel , error ) {
store , err := sqlite . Open ( ctx , sqliteDSN )
if err != nil {
return nil , err
}
defer store . Close ( )
group , err := getLogicalGroupRow ( ctx , store , logicalGroupID )
if err != nil {
return nil , err
}
if ! isPortalLogicalGroupVisible ( group . Status ) {
return nil , fmt . Errorf ( "logical group %q not found" , strings . TrimSpace ( logicalGroupID ) )
}
rows , err := store . LogicalGroupModels ( ) . ListByLogicalGroupID ( ctx , group . LogicalGroupID )
if err != nil {
return nil , err
}
return portalLogicalGroupModelsFromRows ( rows ) , nil
}
}
func buildPortalLogicalGroupInfo ( ctx context . Context , store * sqlite . DB , group sqlite . LogicalGroup ) ( PortalLogicalGroupInfo , error ) {
modelRows , err := store . LogicalGroupModels ( ) . ListByLogicalGroupID ( ctx , group . LogicalGroupID )
if err != nil {
return PortalLogicalGroupInfo { } , err
}
routeRows , err := store . LogicalGroupRoutes ( ) . ListByLogicalGroupID ( ctx , group . LogicalGroupID )
if err != nil {
return PortalLogicalGroupInfo { } , err
}
routeCount := 0
activeRouteCount := 0
for _ , route := range routeRows {
routeCount ++
if strings . EqualFold ( strings . TrimSpace ( route . Status ) , "active" ) {
activeRouteCount ++
}
}
return PortalLogicalGroupInfo {
LogicalGroupID : group . LogicalGroupID ,
DisplayName : group . DisplayName ,
Description : group . Description ,
2026-05-30 10:38:59 +08:00
UsageScenario : group . UsageScenario ,
Recommendation : group . Recommendation ,
NextStepHint : group . NextStepHint ,
2026-05-30 10:54:32 +08:00
VisibilityScope : group . VisibilityScope ,
PackageTier : group . PackageTier ,
PurchaseCTALabel : group . PurchaseCTALabel ,
PurchaseCTAURL : group . PurchaseCTAURL ,
2026-05-29 19:39:41 +08:00
Status : group . Status ,
StickyMode : group . StickyMode ,
RoutePolicy : group . RoutePolicy ,
PublicModels : portalLogicalGroupModelsFromRows ( modelRows ) ,
RouteCount : routeCount ,
ActiveRouteCount : activeRouteCount ,
CreatedAt : group . CreatedAt ,
UpdatedAt : group . UpdatedAt ,
} , nil
}
func portalLogicalGroupModelsFromRows ( rows [ ] sqlite . LogicalGroupModel ) [ ] PortalLogicalGroupModel {
result := make ( [ ] PortalLogicalGroupModel , 0 , len ( rows ) )
for _ , row := range rows {
if ! isPortalLogicalGroupModelVisible ( row . Status ) {
continue
}
result = append ( result , PortalLogicalGroupModel {
PublicModel : row . PublicModel ,
Status : row . Status ,
} )
}
return result
}
func isPortalLogicalGroupVisible ( status string ) bool {
return strings . EqualFold ( strings . TrimSpace ( status ) , "active" )
}
func isPortalLogicalGroupModelVisible ( status string ) bool {
status = strings . TrimSpace ( status )
return status == "" || strings . EqualFold ( status , "active" )
}