init: 初始化 web_hgdjb_server 项目

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 10:47:32 +08:00
co-authored by Claude Opus 4.7
commit f13d33af4d
707 changed files with 147705 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
package appser
import (
"context"
"videoweb/app/api/proto"
"videoweb/pub/common/srvdef"
)
// AppCategoryData 查询应用分类列表及当前分类数据
func AppCategoryData(ctx context.Context, categoryId int64) ([]proto.AppCategoryInfo, proto.AppCategoryInfo, *srvdef.SrvErr) {
return GetAppListCache(), GetCategoryCache(categoryId), nil
}
+196
View File
@@ -0,0 +1,196 @@
package appser
import (
"bytes"
"context"
"encoding/json"
"net/url"
"strings"
"sync"
"videoweb/app/api/proto"
"videoweb/app/service/configser"
"videoweb/pub/common/http"
"videoweb/pub/common/log"
"videoweb/pub/common/netutil"
"videoweb/pub/pubdef"
"videoweb/pub/storage/proxy"
"github.com/robfig/cron/v3"
)
const (
appListAPIPath = "/media/web/appinfo/applist"
)
var jsonHeader = map[string]string{"Content-Type": "application/json"}
var (
appListCron *cron.Cron
appListCache struct {
lock sync.RWMutex
appList []proto.AppCategoryInfo
categoryMaps map[int64]proto.AppCategoryInfo
}
)
type remoteAppListResp struct {
Code int `json:"code"`
Data struct {
List []proto.AppCategoryInfo `json:"list"`
} `json:"data"`
}
// Start 启动定时刷新应用列表缓存(每10分钟一次)
func Start() {
appListCron = cron.New(cron.WithSeconds())
UpdateAppListCache(context.Background())
_, _ = appListCron.AddFunc("0 */10 * * * ?", func() {
UpdateAppListCache(context.Background())
})
appListCron.Start()
}
// GetAppListCache 返回全部应用分类列表(带各分类下应用)
func GetAppListCache() []proto.AppCategoryInfo {
appListCache.lock.RLock()
defer appListCache.lock.RUnlock()
if len(appListCache.appList) == 0 {
return make([]proto.AppCategoryInfo, 0)
}
result := make([]proto.AppCategoryInfo, len(appListCache.appList))
copy(result, appListCache.appList)
return result
}
// GetCategoryCache 按分类ID返回单个分类(含应用列表);categoryId=0 返回第一个分类
func GetCategoryCache(categoryId int64) proto.AppCategoryInfo {
appListCache.lock.RLock()
defer appListCache.lock.RUnlock()
if categoryId == 0 && len(appListCache.appList) > 0 {
return appListCache.appList[0]
}
if v, ok := appListCache.categoryMaps[categoryId]; ok {
return v
}
return proto.AppCategoryInfo{AppList: make([]proto.AppInfoData, 0)}
}
// UpdateAppListCache 从远端拉取应用列表并刷新内存缓存
func UpdateAppListCache(ctx context.Context) {
domain := proxy.DomainAddressRepair("", proxy.BucketVideoAddressV2)
apiURL := strings.TrimSuffix(domain, "/") + appListAPIPath
reqBody, _ := json.Marshal(map[string]string{"dc": "dc"})
resp, err := http.Post(apiURL, jsonHeader, bytes.NewReader(reqBody))
if err != nil {
log.Warn("UpdateAppListCache http.Post failed", log.Any("url", apiURL), log.E(err))
return
}
var remote remoteAppListResp
if err = json.Unmarshal(resp.Data, &remote); err != nil {
log.Warn("UpdateAppListCache unmarshal failed", log.Any("url", apiURL), log.E(err))
return
}
if remote.Code != 200 {
log.Warn("UpdateAppListCache bad code", log.Any("url", apiURL), log.Any("code", remote.Code))
return
}
config := configser.GetConfigInfo()
advitise := configser.GetAdvitise()
channelCode := config.ChannelCode
selfAppName := strings.TrimSpace(config.WebSiteName)
categoryMaps := make(map[int64]proto.AppCategoryInfo)
var appList []proto.AppCategoryInfo
for _, category := range remote.Data.List {
var apps []proto.AppInfoData
for _, app := range category.AppList {
if selfAppName != "" && strings.HasPrefix(app.Name, selfAppName) {
continue
}
apps = append(apps, proto.AppInfoData{
Id: app.Id,
CategoryId: app.CategoryId,
Name: app.Name,
Intro: app.Intro,
Description: app.Description,
Logo: netutil.MergeURL(config.ImgCdn, app.Logo),
Url: appendChannelCode(app.Url, channelCode),
Sort: app.Sort,
})
}
// 广告位 A(code=12)
if category.Code == 12 {
for _, ad := range advitise.AList {
apps = append(apps, proto.AppInfoData{
AdvitiseId: ad.Id,
CategoryId: category.Id,
Name: ad.Title,
Logo: netutil.MergeURL(config.ImgCdn, ad.Images),
Url: ad.Link,
Location: ad.Location,
})
}
}
// 广告位 B(code=13)
if category.Code == 13 {
for _, ad := range advitise.BList {
apps = append(apps, proto.AppInfoData{
AdvitiseId: ad.Id,
CategoryId: category.Id,
Name: ad.Title,
Logo: netutil.MergeURL(config.ImgCdn, ad.Images),
Url: ad.Link,
Location: ad.Location,
})
}
}
if len(apps) == 0 {
continue
}
category.AppList = apps
categoryMaps[category.Id] = proto.AppCategoryInfo{
Id: category.Id,
Title: category.Title,
Intro: category.Intro,
Sort: category.Sort,
Code: category.Code,
}
appList = append(appList, category)
}
appListCache.lock.Lock()
appListCache.appList = appList
appListCache.categoryMaps = categoryMaps
appListCache.lock.Unlock()
}
// appendChannelCode 为应用 URL 拼接渠道码参数 dc=xxx;已含 dc 参数则不再拼接
func appendChannelCode(rawURL, channelCode string) string {
if len(channelCode) == 0 {
channelCode = pubdef.DefaultChannelCode
}
rawURL = strings.TrimSpace(rawURL)
channelCode = strings.TrimSpace(channelCode)
if rawURL == "" || channelCode == "" {
return rawURL
}
parsed, err := url.Parse(rawURL)
if err != nil {
return rawURL
}
query := parsed.Query()
if query.Has("dc") {
return parsed.String()
}
query.Set("dc", channelCode)
parsed.RawQuery = query.Encode()
return parsed.String()
}
+52
View File
@@ -0,0 +1,52 @@
package appser
import (
"strings"
"testing"
"videoweb/pub/pubdef"
)
func TestAppendChannelCode(t *testing.T) {
t.Parallel()
tests := []struct {
name string
rawURL string
channelCode string
wantContain string
wantSkip bool
}{
{
name: "append custom channel",
rawURL: "https://example.com/app",
channelCode: "custom",
wantContain: "dc=custom",
},
{
name: "fallback default channel",
rawURL: "https://example.com/app",
channelCode: "",
wantContain: "dc=" + pubdef.DefaultChannelCode,
},
{
name: "skip existing dc",
rawURL: "https://example.com/app?dc=exists",
channelCode: "custom",
wantContain: "dc=exists",
wantSkip: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := appendChannelCode(tt.rawURL, tt.channelCode)
if !strings.Contains(got, tt.wantContain) {
t.Fatalf("appendChannelCode() = %q, want contain %q", got, tt.wantContain)
}
if tt.wantSkip && strings.Contains(got, "dc=custom") {
t.Fatalf("appendChannelCode() should keep existing dc: %q", got)
}
})
}
}