spark-server/deploy/popadmin/price-package-patch/AdminManageController.java
DESKTOP-L8OUL9F\gfl0034 5d05eb0804 chore(deploy): geo snippet、popadmin 补丁与订阅包名脚本
补充 world geo nginx snippet、OpenAPI/geo 热更新脚本,以及 popadmin package_name 定价补丁,便于运维对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 09:17:18 +08:00

1369 lines
72 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.aliyun.control.dto.ApiResponse
* com.aliyun.control.exception.BusinessException
* com.wyrs.admin.config.ApiDocCatalog
* com.wyrs.admin.config.WyrsAppRegistry
* com.wyrs.admin.controller.AdminManageController
* com.wyrs.admin.controller.AdminManageController$DashboardCache
* com.wyrs.admin.entity.AdminOperationLog
* com.wyrs.admin.entity.AppAdPlacement
* com.wyrs.admin.entity.AppApkDownload
* com.wyrs.admin.entity.AppBanner
* com.wyrs.admin.entity.AppClientFeature
* com.wyrs.admin.entity.AppSiteContent
* com.wyrs.admin.entity.AppUser
* com.wyrs.admin.entity.AppVersion
* com.wyrs.admin.entity.AppWhitelistEntry
* com.wyrs.admin.entity.ClientLog
* com.wyrs.admin.entity.DailyActiveStat
* com.wyrs.admin.entity.DeveloperApp
* com.wyrs.admin.entity.MembershipPrice
* com.wyrs.admin.entity.PaymentChannelConfig
* com.wyrs.admin.entity.PaymentRecord
* com.wyrs.admin.entity.UserFeedback
* com.wyrs.admin.repository.AppUserRepository
* com.wyrs.admin.repository.ClientLogRepository
* com.wyrs.admin.repository.DailyActiveStatRepository
* com.wyrs.admin.repository.PaymentRecordRepository
* com.wyrs.admin.repository.UserFeedbackRepository
* com.wyrs.admin.security.AdminSiteScope
* com.wyrs.admin.security.AdminSiteScope$Kind
* com.wyrs.admin.service.AdminStatsService
* com.wyrs.admin.service.AdminStatsService$DateRange
* com.wyrs.admin.service.AdminUserIpEnricher
* com.wyrs.admin.service.ApkUploadService
* com.wyrs.admin.service.AppClientService
* com.wyrs.admin.service.AppUserLookupService
* com.wyrs.admin.service.BannerImageService
* com.wyrs.admin.service.BannerStateService
* com.wyrs.admin.service.CapacityPlanService
* com.wyrs.admin.service.DauCleanupService
* com.wyrs.admin.service.DeveloperAppIconService
* com.wyrs.admin.service.JavaPageMetricsService
* com.wyrs.admin.service.OnlineDeviceTracker
* com.wyrs.admin.service.OperationLogService
* com.wyrs.admin.service.ServerMonitorService
* com.wyrs.admin.service.config.AdPlacementsConfigService
* com.wyrs.admin.service.config.AdminConfigDbMirrorService
* com.wyrs.admin.service.config.ApkDownloadsConfigService
* com.wyrs.admin.service.config.AppClientFeatureConfigService
* com.wyrs.admin.service.config.AppSiteContentConfigService
* com.wyrs.admin.service.config.AppWhitelistConfigService
* com.wyrs.admin.service.config.BannersConfigService
* com.wyrs.admin.service.config.DeveloperAppsConfigService
* com.wyrs.admin.service.config.PaymentChannelConfigService
* com.wyrs.admin.service.config.PricesConfigService
* com.wyrs.admin.service.config.VersionsConfigService
* com.wyrs.admin.util.AppChannelNames
* com.wyrs.offerwall.config.OfferwallPackages
* org.springframework.data.domain.Page
* org.springframework.data.domain.PageImpl
* org.springframework.data.domain.PageRequest
* org.springframework.data.domain.Pageable
* org.springframework.data.domain.Sort
* org.springframework.data.domain.Sort$Direction
* org.springframework.http.HttpStatus
* org.springframework.http.MediaType
* org.springframework.http.ResponseEntity
* org.springframework.http.ResponseEntity$BodyBuilder
* org.springframework.web.bind.annotation.DeleteMapping
* org.springframework.web.bind.annotation.GetMapping
* org.springframework.web.bind.annotation.PatchMapping
* org.springframework.web.bind.annotation.PathVariable
* org.springframework.web.bind.annotation.PostMapping
* org.springframework.web.bind.annotation.PutMapping
* org.springframework.web.bind.annotation.RequestBody
* org.springframework.web.bind.annotation.RequestMapping
* org.springframework.web.bind.annotation.RequestParam
* org.springframework.web.bind.annotation.RestController
* org.springframework.web.multipart.MultipartFile
*/
package com.wyrs.admin.controller;
import com.aliyun.control.dto.ApiResponse;
import com.aliyun.control.exception.BusinessException;
import com.wyrs.admin.config.ApiDocCatalog;
import com.wyrs.admin.config.WyrsAppRegistry;
import com.wyrs.admin.controller.AdminManageController;
import com.wyrs.admin.entity.AdminOperationLog;
import com.wyrs.admin.entity.AppAdPlacement;
import com.wyrs.admin.entity.AppApkDownload;
import com.wyrs.admin.entity.AppBanner;
import com.wyrs.admin.entity.AppClientFeature;
import com.wyrs.admin.entity.AppSiteContent;
import com.wyrs.admin.entity.AppUser;
import com.wyrs.admin.entity.AppVersion;
import com.wyrs.admin.entity.AppWhitelistEntry;
import com.wyrs.admin.entity.ClientLog;
import com.wyrs.admin.entity.DailyActiveStat;
import com.wyrs.admin.entity.DeveloperApp;
import com.wyrs.admin.entity.MembershipPrice;
import com.wyrs.admin.entity.PaymentChannelConfig;
import com.wyrs.admin.entity.PaymentRecord;
import com.wyrs.admin.entity.UserFeedback;
import com.wyrs.admin.repository.AppUserRepository;
import com.wyrs.admin.repository.ClientLogRepository;
import com.wyrs.admin.repository.DailyActiveStatRepository;
import com.wyrs.admin.repository.PaymentRecordRepository;
import com.wyrs.admin.repository.UserFeedbackRepository;
import com.wyrs.admin.security.AdminSiteScope;
import com.wyrs.admin.service.AdminStatsService;
import com.wyrs.admin.service.AdminUserIpEnricher;
import com.wyrs.admin.service.ApkUploadService;
import com.wyrs.admin.service.AppClientService;
import com.wyrs.admin.service.AppUserLookupService;
import com.wyrs.admin.service.BannerImageService;
import com.wyrs.admin.service.BannerStateService;
import com.wyrs.admin.service.CapacityPlanService;
import com.wyrs.admin.service.DauCleanupService;
import com.wyrs.admin.service.DeveloperAppIconService;
import com.wyrs.admin.service.JavaPageMetricsService;
import com.wyrs.admin.service.OnlineDeviceTracker;
import com.wyrs.admin.service.OperationLogService;
import com.wyrs.admin.service.ServerMonitorService;
import com.wyrs.admin.service.config.AdPlacementsConfigService;
import com.wyrs.admin.service.config.AdminConfigDbMirrorService;
import com.wyrs.admin.service.config.ApkDownloadsConfigService;
import com.wyrs.admin.service.config.AppClientFeatureConfigService;
import com.wyrs.admin.service.config.AppSiteContentConfigService;
import com.wyrs.admin.service.config.AppWhitelistConfigService;
import com.wyrs.admin.service.config.BannersConfigService;
import com.wyrs.admin.service.config.DeveloperAppsConfigService;
import com.wyrs.admin.service.config.PaymentChannelConfigService;
import com.wyrs.admin.service.config.PricesConfigService;
import com.wyrs.admin.service.config.VersionsConfigService;
import com.wyrs.admin.util.AppChannelNames;
import com.wyrs.offerwall.config.OfferwallPackages;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/*
* Exception performing whole class analysis ignored.
*/
@RestController
@RequestMapping(value={"/api/admin"})
public class AdminManageController {
private final AppUserRepository appUserRepository;
private final ClientLogRepository clientLogRepository;
private final UserFeedbackRepository userFeedbackRepository;
private final PaymentRecordRepository paymentRecordRepository;
private final DailyActiveStatRepository dailyActiveStatRepository;
private final OperationLogService operationLogService;
private final AdminStatsService adminStatsService;
private final ServerMonitorService serverMonitorService;
private final JavaPageMetricsService javaPageMetricsService;
private final CapacityPlanService capacityPlanService;
private final DauCleanupService dauCleanupService;
private final BannerImageService bannerImageService;
private final BannerStateService bannerStateService;
private final BannersConfigService bannersConfigService;
private final VersionsConfigService versionsConfigService;
private final PricesConfigService pricesConfigService;
private final PaymentChannelConfigService paymentChannelConfigService;
private final DeveloperAppsConfigService developerAppsConfigService;
private final AdPlacementsConfigService adPlacementsConfigService;
private final ApkDownloadsConfigService apkDownloadsConfigService;
private final AppSiteContentConfigService siteContentConfigService;
private final AppWhitelistConfigService appWhitelistConfigService;
private final AppClientFeatureConfigService appClientFeatureConfigService;
private final ApkUploadService apkUploadService;
private final DeveloperAppIconService developerAppIconService;
private final AdminConfigDbMirrorService configDbMirrorService;
private final AppUserLookupService appUserLookupService;
private final AppClientService appClientService;
private final OnlineDeviceTracker onlineDeviceTracker;
private final AdminUserIpEnricher adminUserIpEnricher;
private volatile DashboardCache dashboardCache;
private static final long DASHBOARD_CACHE_MS = 60000L;
@GetMapping(value={"/site-scope"})
public ApiResponse<Map<String, Object>> siteScope() {
return ApiResponse.ok((Object)AdminSiteScope.current().toPublicMap());
}
@GetMapping(value={"/apps"})
public ApiResponse<List<Map<String, Object>>> apps() {
AdminSiteScope scope = AdminSiteScope.current();
if (scope.kind() == AdminSiteScope.Kind.ADADMIN) {
ArrayList rows = new ArrayList();
for (Map opt : OfferwallPackages.adminOptions()) {
LinkedHashMap<String, Object> row = new LinkedHashMap<String, Object>();
row.put("code", "offerwall");
row.put("name", opt.get("label"));
row.put("packageName", opt.get("packageName"));
row.put("packageNames", List.of((String)opt.get("packageName")));
row.put("siteRealm", "ADADMIN");
rows.add(row);
}
return ApiResponse.ok(rows);
}
return ApiResponse.ok((Object)WyrsAppRegistry.toAdminList((Collection)(scope.isFull() ? null : scope.appCodes())));
}
@GetMapping(value={"/api-doc"})
public ApiResponse<Map<String, Object>> apiDoc() {
Map doc = ApiDocCatalog.toApiDoc();
AdminSiteScope scope = AdminSiteScope.current();
doc.put("siteScope", scope.toPublicMap());
if (!scope.isFull()) {
if (scope.kind() == AdminSiteScope.Kind.ADADMIN) {
ArrayList rows = new ArrayList();
for (Map opt : OfferwallPackages.adminOptions()) {
LinkedHashMap<String, Object> row = new LinkedHashMap<String, Object>();
row.put("code", "offerwall");
row.put("name", opt.get("label"));
row.put("packageName", opt.get("packageName"));
row.put("packageNames", List.of((String)opt.get("packageName")));
rows.add(row);
}
doc.put("apps", rows);
} else {
doc.put("apps", WyrsAppRegistry.toAdminList((Collection)scope.appCodes()));
}
doc.put("baseUrl", "https://" + (scope.host().isBlank() ? "admin.wyrs365.com" : scope.host()));
}
return ApiResponse.ok((Object)doc);
}
@GetMapping(value={"/channels"})
public ApiResponse<List<Map<String, String>>> channels() {
return ApiResponse.ok((Object)AppChannelNames.toAdminList());
}
@GetMapping(value={"/developer-app-channels"})
public ApiResponse<List<Map<String, String>>> developerAppChannels() {
return ApiResponse.ok((Object)AppChannelNames.toDeveloperAppAdminList());
}
/*
* WARNING - Removed try catching itself - possible behaviour change.
*/
@GetMapping(value={"/dashboard"})
public ApiResponse<Map<String, Object>> dashboard() {
AdminSiteScope scope = AdminSiteScope.current();
if (!scope.isFull()) {
return ApiResponse.ok((Object)this.buildDashboard());
}
long now = System.currentTimeMillis();
DashboardCache cached = this.dashboardCache;
if (cached != null && now - cached.atMs < 60000L) {
return ApiResponse.ok((Object)cached.data);
}
AdminManageController adminManageController = this;
synchronized (adminManageController) {
cached = this.dashboardCache;
if (cached != null && now - cached.atMs < 60000L) {
return ApiResponse.ok((Object)cached.data);
}
Map data = this.buildDashboard();
this.dashboardCache = new DashboardCache(new LinkedHashMap(data), now);
return ApiResponse.ok((Object)data);
}
}
private Map<String, Object> buildDashboard() {
ArrayList codes;
LocalDate today = AdminStatsService.todayInChina();
AdminSiteScope scope = AdminSiteScope.current();
Map dauSummary = this.scopedDauSummary(scope);
LinkedHashMap<String, Object> data = new LinkedHashMap<String, Object>();
ArrayList arrayList = codes = scope.isFull() ? List.of() : new ArrayList(scope.appCodes());
if (codes.isEmpty()) {
data.put("totalUsers", this.appUserRepository.count());
} else {
long total = 0L;
for (String c : codes) {
total += this.appUserRepository.countByAppCode(c);
}
data.put("totalUsers", total);
}
data.put("todayActive", dauSummary.get("todayTotal"));
if (codes.isEmpty()) {
data.put("pendingFeedback", this.userFeedbackRepository.findByStatus("pending", (Pageable)PageRequest.of((int)0, (int)1)).getTotalElements());
data.put("totalPayments", this.paymentRecordRepository.count());
} else {
long pending = 0L;
long payments = 0L;
for (String c : codes) {
pending += this.userFeedbackRepository.countByStatusAndAppCode("pending", c);
payments += this.paymentRecordRepository.countByAppCode(c);
}
data.put("pendingFeedback", pending);
data.put("totalPayments", payments);
}
LinkedHashMap byApp = this.adminStatsService.dashboardByApps();
if (!scope.isFull()) {
LinkedHashMap filtered = new LinkedHashMap();
for (String c : scope.appCodes()) {
if (!byApp.containsKey(c)) continue;
filtered.put(c, byApp.get(c));
}
byApp = filtered;
}
data.put("byApp", byApp);
data.put("dauSummary", dauSummary);
LinkedHashMap country = this.adminStatsService.countryDauByApps(today);
if (!scope.isFull() && country != null) {
LinkedHashMap filteredCountry = new LinkedHashMap();
for (String c : scope.appCodes()) {
if (!country.containsKey(c)) continue;
filteredCountry.put(c, country.get(c));
}
country = filteredCountry;
}
data.put("countryDauByApp", country);
data.put("capacityPlan", this.capacityPlanService.plan());
data.put("siteScope", scope.toPublicMap());
return data;
}
@GetMapping(value={"/dau/summary"})
public ApiResponse<Map<String, Object>> dauSummary() {
return ApiResponse.ok((Object)this.scopedDauSummary(AdminSiteScope.current()));
}
@GetMapping(value={"/retention/summary"})
public ApiResponse<Map<String, Object>> retentionSummary(@RequestParam(required=false) String appCode, @RequestParam(defaultValue="14") int days) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)appCode);
if (code != null && !code.isBlank()) {
WyrsAppRegistry.require((String)code);
return ApiResponse.ok((Object)this.adminStatsService.retentionSummary(code, days));
}
Map summary = this.adminStatsService.retentionSummary(null, days);
if (!scope.isFull()) {
summary = AdminManageController.filterRetentionSummary((Map)summary, (Set)scope.appCodes());
}
return ApiResponse.ok((Object)summary);
}
@GetMapping(value={"/stats/hourly-24h"})
public ApiResponse<Map<String, Object>> hourly24h(@RequestParam(required=false) String appCode, @RequestParam(required=false, defaultValue="yesterday") String compare) {
AdminSiteScope scope = AdminSiteScope.current();
String code = null;
if (appCode != null && !appCode.isBlank()) {
code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)appCode);
WyrsAppRegistry.require((String)code);
} else if (!scope.isFull()) {
Map full = this.adminStatsService.hourly24h(null);
return ApiResponse.ok((Object)AdminManageController.filterHourly24h((Map)full, (Set)scope.appCodes()));
}
if (compare == null || compare.isBlank() || "yesterday".equalsIgnoreCase(compare.trim()) || !"none".equalsIgnoreCase(compare.trim())) {
// empty if block
}
return ApiResponse.ok((Object)this.adminStatsService.hourly24h(code));
}
private static Map<String, Object> filterHourly24h(Map<String, Object> summary, Set<String> allowed) {
if (summary == null || allowed == null || allowed.isEmpty()) {
return summary;
}
LinkedHashMap<String, Object> out = new LinkedHashMap<String, Object>(summary);
ArrayList<Map> filtered = new ArrayList<Map>();
long[] newTotal = null;
long[] activeTotal = null;
long[] yNewTotal = null;
long[] yActiveTotal = null;
Object appsObj = summary.get("apps");
if (appsObj instanceof List) {
List list = (List)appsObj;
for (Object o : list) {
Map m;
Object c;
if (!(o instanceof Map) || (c = (m = (Map)o).get("appCode")) == null || !allowed.contains(String.valueOf(c))) continue;
filtered.add(m);
Object pts = m.get("points");
if (!(pts instanceof List)) continue;
List points = (List)pts;
if (newTotal == null) {
newTotal = new long[points.size()];
activeTotal = new long[points.size()];
yNewTotal = new long[points.size()];
yActiveTotal = new long[points.size()];
}
for (int i = 0; i < points.size(); ++i) {
Object e = points.get(i);
if (!(e instanceof Map)) continue;
Map p = (Map)e;
Object nu = p.get("newUser");
Object au = p.get("activeUser");
Object ynu = p.get("yesterdayNewUser");
Object yau = p.get("yesterdayActiveUser");
if (nu instanceof Number) {
Number n = (Number)nu;
int n2 = i;
newTotal[n2] = newTotal[n2] + n.longValue();
}
if (au instanceof Number) {
Number a = (Number)au;
int n = i;
activeTotal[n] = activeTotal[n] + a.longValue();
}
if (ynu instanceof Number) {
Number yn = (Number)ynu;
int n = i;
yNewTotal[n] = yNewTotal[n] + yn.longValue();
}
if (!(yau instanceof Number)) continue;
Number ya = (Number)yau;
int n = i;
yActiveTotal[n] = yActiveTotal[n] + ya.longValue();
}
}
}
out.put("apps", filtered);
LinkedHashMap<String, Object> total = new LinkedHashMap<String, Object>();
total.put("appCode", null);
total.put("name", "\u603b\u670d\u52a1\u5668");
if (newTotal != null && !filtered.isEmpty()) {
Object samplePts = ((Map)filtered.get(0)).get("points");
ArrayList points = new ArrayList();
if (samplePts instanceof List) {
List sp = (List)samplePts;
for (int i = 0; i < sp.size(); ++i) {
LinkedHashMap p = new LinkedHashMap();
Object e = sp.get(i);
if (e instanceof Map) {
Map src = (Map)e;
p.put("ts", src.get("ts"));
p.put("time", src.get("time"));
p.put("label", src.get("label"));
p.put("yesterdayTs", src.get("yesterdayTs"));
p.put("yesterdayTime", src.get("yesterdayTime"));
}
p.put("newUser", newTotal[i]);
p.put("activeUser", activeTotal[i]);
p.put("yesterdayNewUser", yNewTotal[i]);
p.put("yesterdayActiveUser", yActiveTotal[i]);
points.add(p);
}
}
total.put("points", points);
} else {
total.put("points", List.of());
}
out.put("total", total);
return out;
}
private Map<String, Object> scopedDauSummary(AdminSiteScope scope) {
Map summary = this.adminStatsService.dauSummary();
if (scope == null || scope.isFull()) {
return summary;
}
Set allowed = scope.appCodes();
List apps = (List)summary.get("apps");
ArrayList<Map> filtered = new ArrayList<Map>();
long todayTotal = 0L;
long yesterdayTotal = 0L;
long todayNewTotal = 0L;
long yesterdayNewTotal = 0L;
long todayIos = 0L;
long todayAndroid = 0L;
long yesterdayIos = 0L;
long yesterdayAndroid = 0L;
long totalUsers = 0L;
if (apps != null) {
for (Map row : apps) {
Object code = row.get("appCode");
if (code == null || !allowed.contains(String.valueOf(code))) continue;
filtered.add(row);
todayTotal += AdminManageController.toLong(row.get("todayDau"));
yesterdayTotal += AdminManageController.toLong(row.get("yesterdayDau"));
todayNewTotal += AdminManageController.toLong(row.get("todayNewDau"));
yesterdayNewTotal += AdminManageController.toLong(row.get("yesterdayNewDau"));
todayIos += AdminManageController.toLong(row.get("todayIos"));
todayAndroid += AdminManageController.toLong(row.get("todayAndroid"));
yesterdayIos += AdminManageController.toLong(row.get("yesterdayIos"));
yesterdayAndroid += AdminManageController.toLong(row.get("yesterdayAndroid"));
totalUsers += AdminManageController.toLong(row.get("totalUsers"));
}
}
LinkedHashMap<String, Object> out = new LinkedHashMap<String, Object>(summary);
out.put("apps", filtered);
out.put("todayTotal", todayTotal);
out.put("yesterdayTotal", yesterdayTotal);
out.put("todayNewTotal", todayNewTotal);
out.put("yesterdayNewTotal", yesterdayNewTotal);
out.put("todayIos", todayIos);
out.put("todayAndroid", todayAndroid);
out.put("yesterdayIos", yesterdayIos);
out.put("yesterdayAndroid", yesterdayAndroid);
out.put("totalUsers", totalUsers);
out.put("siteScope", scope.toPublicMap());
return out;
}
private static Map<String, Object> filterRetentionSummary(Map<String, Object> summary, Set<String> allowed) {
if (summary == null || allowed == null || allowed.isEmpty()) {
return summary;
}
LinkedHashMap<String, Object> out = new LinkedHashMap<String, Object>(summary);
Object appsObj = summary.get("apps");
if (appsObj instanceof List) {
List list = (List)appsObj;
ArrayList<Map> filtered = new ArrayList<Map>();
for (Object o : list) {
Map m;
Object code;
if (!(o instanceof Map) || (code = (m = (Map)o).get("appCode")) == null || !allowed.contains(String.valueOf(code))) continue;
filtered.add(m);
}
out.put("apps", filtered);
}
return out;
}
private static long toLong(Object v) {
if (v instanceof Number) {
Number n = (Number)v;
return n.longValue();
}
if (v == null) {
return 0L;
}
try {
return Long.parseLong(String.valueOf(v));
}
catch (Exception e) {
return 0L;
}
}
@GetMapping(value={"/monitor"})
public ApiResponse<Map<String, Object>> monitor() {
return ApiResponse.ok((Object)this.serverMonitorService.snapshot());
}
@GetMapping(value={"/monitor/java-breakdown"})
public ApiResponse<Map<String, Object>> javaBreakdown() {
Map snap = this.serverMonitorService.snapshot();
double javaCpu = AdminManageController.extractJavaCpuPercent((Map)snap);
return ApiResponse.ok((Object)this.javaPageMetricsService.breakdown(javaCpu));
}
@GetMapping(value={"/monitor/full"})
public ApiResponse<Map<String, Object>> monitorFull() {
Map snap = this.serverMonitorService.snapshot();
double javaCpu = AdminManageController.extractJavaCpuPercent((Map)snap);
LinkedHashMap<String, Map> data = new LinkedHashMap<String, Map>(snap);
data.put("javaBreakdown", this.javaPageMetricsService.breakdown(javaCpu));
data.put("onlineDevices", this.onlineDeviceTracker.series24h());
return ApiResponse.ok(data);
}
@GetMapping(value={"/online-devices/series"})
public ApiResponse<Map<String, Object>> onlineDevicesSeries() {
return ApiResponse.ok((Object)this.onlineDeviceTracker.series24h());
}
private static double extractJavaCpuPercent(Map<String, Object> snap) {
double d;
Object rows = snap.get("cpuBreakdown");
if (!(rows instanceof List)) {
return 0.0;
}
List list = (List)rows;
for (Object item : list) {
Object cpu;
Map row;
if (!(item instanceof Map) || !"java".equals(String.valueOf((row = (Map)item).get("key"))) || !((cpu = row.get("cpuPercent")) instanceof Number)) continue;
Number n = (Number)cpu;
return n.doubleValue();
}
Object cpu = snap.get("cpuPercent");
if (cpu instanceof Number) {
Number n = (Number)cpu;
d = n.doubleValue();
} else {
d = 0.0;
}
return d;
}
@GetMapping(value={"/capacity-plan"})
public ApiResponse<Map<String, Object>> capacityPlan() {
return ApiResponse.ok((Object)this.capacityPlanService.plan());
}
@GetMapping(value={"/users"})
public ApiResponse<Page<Map<String, Object>>> users(@RequestParam(name="appCode", required=false) String appCode, @RequestParam(name="phone", required=false) String phone, @RequestParam(name="email", required=false) String email, @RequestParam(name="id", required=false) Long id, @RequestParam(name="page", defaultValue="0") int page, @RequestParam(name="size", defaultValue="20") int size) {
AdminSiteScope scope = AdminSiteScope.current();
PageRequest pr = PageRequest.of((int)page, (int)size, (Sort)Sort.by((Sort.Direction)Sort.Direction.DESC, (String[])new String[]{"id"}));
List scopeCodes = scope.queryAppCodes(appCode);
if (id != null) {
return this.appUserRepository.findById((Object)id).map(u -> {
if (!scope.allows(u.getAppCode())) {
return ApiResponse.ok((Object)Page.empty((Pageable)pr));
}
if (!(appCode == null || appCode.isBlank() || appCode.equalsIgnoreCase(u.getAppCode()) || ("spark".equalsIgnoreCase(appCode) || "pop".equalsIgnoreCase(appCode)) && ("spark".equalsIgnoreCase(u.getAppCode()) || "pop".equalsIgnoreCase(u.getAppCode())))) {
return ApiResponse.ok((Object)Page.empty((Pageable)pr));
}
return ApiResponse.ok((Object)new PageImpl(List.of(this.adminUserIpEnricher.toListRow(u)), (Pageable)pr, 1L));
}).orElseGet(() -> ApiResponse.ok((Object)Page.empty((Pageable)pr)));
}
if (email != null && !email.isBlank()) {
String e = email.trim().toLowerCase(Locale.ROOT);
List<AppUser> matched = this.appUserRepository.findByEmail(e);
if (!scopeCodes.isEmpty()) {
matched = matched.stream().filter(u -> scopeCodes.contains(u.getAppCode() == null ? "" : u.getAppCode().toLowerCase(Locale.ROOT))).toList();
} else if (!scope.isFull()) {
matched = matched.stream().filter(u -> scope.allows(u.getAppCode())).toList();
}
return ApiResponse.ok((Object)this.enrichUserPage(matched, pr, (long)matched.size()));
}
if (phone != null && !phone.isBlank()) {
Page p = this.appUserRepository.findByPhoneContaining(phone, (Pageable)pr);
if (scope.isFull() && scopeCodes.isEmpty()) {
return ApiResponse.ok((Object)this.enrichUserPage(p));
}
List<AppUser> filtered = p.getContent().stream().filter(u -> scope.allows(u.getAppCode())).toList();
return ApiResponse.ok((Object)this.enrichUserPage(filtered, pr, (long)filtered.size()));
}
if (!scopeCodes.isEmpty()) {
if (scopeCodes.size() == 1) {
return ApiResponse.ok((Object)this.enrichUserPage(this.appUserRepository.findByAppCode((String)scopeCodes.get(0), (Pageable)pr)));
}
return ApiResponse.ok((Object)this.enrichUserPage(this.appUserRepository.findByAppCodeIn((Collection)scopeCodes, (Pageable)pr)));
}
if (!scope.isFull()) {
return ApiResponse.ok((Object)this.enrichUserPage(this.appUserRepository.findByAppCodeIn(new ArrayList(scope.appCodes()), (Pageable)pr)));
}
return ApiResponse.ok((Object)this.enrichUserPage(this.appUserRepository.findAll((Pageable)pr)));
}
private Page<Map<String, Object>> enrichUserPage(Page<AppUser> page) {
List<Map> rows = page.getContent().stream().map(arg_0 -> ((AdminUserIpEnricher)this.adminUserIpEnricher).toListRow(arg_0)).toList();
return new PageImpl(rows, page.getPageable(), page.getTotalElements());
}
private Page<Map<String, Object>> enrichUserPage(List<AppUser> users, PageRequest pr, long total) {
List<Map> rows = users.stream().map(arg_0 -> ((AdminUserIpEnricher)this.adminUserIpEnricher).toListRow(arg_0)).toList();
return new PageImpl(rows, (Pageable)pr, total);
}
@GetMapping(value={"/users/by-email"})
public ApiResponse<Map<String, Object>> userByEmail(@RequestParam String email, @RequestParam(required=false) String appCode) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)appCode);
Map data = this.appUserLookupService.lookupByEmail(email, code);
if (!scope.isFull() && scope.kind() == AdminSiteScope.Kind.POPADMIN && (appCode == null || appCode.isBlank())) {
List list;
List list2;
Map xz = this.appUserLookupService.lookupByEmail(email, "xiaoxiaozhou");
Object v = data.get("appUsers");
if (v instanceof List) {
List l = (List)v;
list2 = (List)data.get("appUsers");
} else {
list2 = List.of();
}
List sparkUsers = list2;
Object v2 = xz.get("appUsers");
if (v2 instanceof List) {
List l = (List)v2;
list = (List)xz.get("appUsers");
} else {
list = List.of();
}
List xzUsers = list;
ArrayList merged = new ArrayList(sparkUsers);
merged.addAll(xzUsers);
data.put("appUsers", merged);
data.put("appUserCount", merged.size());
}
if (!scope.isFull()) {
AdminManageController.filterScopedUserLookup((Map)data, (AdminSiteScope)scope);
}
data.put("siteScope", scope.toPublicMap());
return ApiResponse.ok((Object)data);
}
@GetMapping(value={"/users/by-nickname"})
public ApiResponse<Map<String, Object>> userByNickname(@RequestParam String nickname, @RequestParam(required=false) String appCode) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)appCode);
Map data = this.appUserLookupService.lookupByNickname(nickname, code);
data.put("siteScope", scope.toPublicMap());
return ApiResponse.ok((Object)data);
}
@GetMapping(value={"/users/by-device"})
public ApiResponse<Map<String, Object>> userByDevice(@RequestParam String deviceId, @RequestParam(required=false) String appCode) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)appCode);
Map data = this.appUserLookupService.lookupByDeviceId(deviceId, code);
data.put("siteScope", scope.toPublicMap());
return ApiResponse.ok((Object)data);
}
@GetMapping(value={"/users/by-ip"})
public ApiResponse<Map<String, Object>> userByIp(@RequestParam String ip, @RequestParam(required=false) String appCode) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)appCode);
Map data = this.appUserLookupService.lookupByIp(ip, code);
data.put("siteScope", scope.toPublicMap());
return ApiResponse.ok((Object)data);
}
@GetMapping(value={"/users/export-zip"})
public ResponseEntity<byte[]> exportUserZip(@RequestParam String email, @RequestParam(required=false) String appCode) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)appCode);
if (code == null || code.isBlank()) {
code = "spark";
}
byte[] zip = this.appUserLookupService.exportUserZip(email, code);
String filename = "user-" + email.replaceAll("[^a-zA-Z0-9@._-]", "_") + ".zip";
return ((ResponseEntity.BodyBuilder)ResponseEntity.ok().header("Content-Disposition", new String[]{"attachment; filename=\"" + filename + "\""})).contentType(MediaType.parseMediaType((String)"application/zip")).body((Object)zip);
}
@PostMapping(value={"/users/by-email/reset-avatar"})
public ApiResponse<Map<String, Object>> resetAvatarByEmail(@RequestBody Map<String, Object> body) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)AdminManageController.str(body, (String)"appCode"));
return ApiResponse.ok((Object)this.appUserLookupService.resetAvatar(AdminManageController.str(body, (String)"email"), code));
}
@PostMapping(value={"/users/by-email/reset-nickname"})
public ApiResponse<Map<String, Object>> resetNicknameByEmail(@RequestBody Map<String, Object> body) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)AdminManageController.str(body, (String)"appCode"));
return ApiResponse.ok((Object)this.appUserLookupService.resetNickname(AdminManageController.str(body, (String)"email"), code));
}
@PostMapping(value={"/users/by-email/mark-abnormal"})
public ApiResponse<Map<String, Object>> markAbnormalByEmail(@RequestBody Map<String, Object> body) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)AdminManageController.str(body, (String)"appCode"));
boolean abnormal = body.get("abnormal") == null || Boolean.TRUE.equals(body.get("abnormal"));
return ApiResponse.ok((Object)this.appUserLookupService.markAbnormal(AdminManageController.str(body, (String)"email"), code, abnormal, AdminManageController.str(body, (String)"reason")));
}
@PostMapping(value={"/users/by-email/ban"})
public ApiResponse<Map<String, Object>> banUserByEmail(@RequestBody Map<String, Object> body) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)AdminManageController.str(body, (String)"appCode"));
boolean banned = body.get("banned") == null || Boolean.TRUE.equals(body.get("banned"));
return ApiResponse.ok((Object)this.appUserLookupService.banUser(AdminManageController.str(body, (String)"email"), code, banned, AdminManageController.str(body, (String)"reason")));
}
@PostMapping(value={"/users/by-email/whitelist"})
public ApiResponse<Map<String, Object>> whitelistByEmail(@RequestBody Map<String, Object> body) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)AdminManageController.str(body, (String)"appCode"));
if (code == null || code.isBlank()) {
throw new BusinessException("\u8bf7\u6307\u5b9a appCode", HttpStatus.BAD_REQUEST);
}
return ApiResponse.ok((Object)this.appUserLookupService.addWhitelist(AdminManageController.str(body, (String)"email"), code, AdminManageController.str(body, (String)"note")));
}
@PostMapping(value={"/users/by-email/reset-password"})
public ApiResponse<Map<String, Object>> resetPasswordByEmail(@RequestBody Map<String, Object> body) {
AdminSiteScope scope = AdminSiteScope.current();
String code = AdminManageController.resolveScopedAppCode((AdminSiteScope)scope, (String)AdminManageController.str(body, (String)"appCode"));
String newPassword = AdminManageController.str(body, (String)"newPassword");
if (newPassword == null || newPassword.isBlank()) {
newPassword = AdminManageController.str(body, (String)"password");
}
return ApiResponse.ok((Object)this.appUserLookupService.resetPassword(AdminManageController.str(body, (String)"email"), code, newPassword));
}
private static String str(Map<String, Object> body, String key) {
Object v = body == null ? null : body.get(key);
return v == null ? null : String.valueOf(v);
}
private static String resolveScopedAppCode(AdminSiteScope scope, String appCode) {
if (appCode != null && !appCode.isBlank()) {
scope.assertAllowed(appCode);
return appCode.trim().toLowerCase(Locale.ROOT);
}
String def = scope.defaultAppCodeOrNull();
if (def != null) {
return def;
}
if (scope.kind() == AdminSiteScope.Kind.POPADMIN) {
return "spark";
}
return null;
}
private static void filterScopedUserLookup(Map<String, Object> data, AdminSiteScope scope) {
Object sameDevice;
Object linked = data.get("linkedApps");
if (linked instanceof List) {
List list = (List)linked;
ArrayList<Map> filtered = new ArrayList<Map>();
for (Object o : list) {
Map m;
Object code;
if (!(o instanceof Map) || (code = (m = (Map)o).get("appCode")) == null || !scope.allows(String.valueOf(code))) continue;
filtered.add(m);
}
data.put("linkedApps", filtered);
data.put("linkedAppCount", filtered.size());
}
if ((sameDevice = data.get("sameDeviceApps")) instanceof List) {
List list = (List)sameDevice;
ArrayList<Map> filtered = new ArrayList<Map>();
for (Object o : list) {
Map m;
Object code;
if (!(o instanceof Map) || (code = (m = (Map)o).get("appCode")) == null || !scope.allows(String.valueOf(code))) continue;
filtered.add(m);
}
data.put("sameDeviceApps", filtered);
data.put("sameDeviceAppCount", filtered.size());
}
}
@GetMapping(value={"/users/{id}"})
public ApiResponse<Map<String, Object>> userDetail(@PathVariable Long id) {
AppUser u;
AdminSiteScope scope = AdminSiteScope.current();
if (!scope.allows((u = (AppUser)this.appUserRepository.findById((Object)id).orElseThrow(() -> new BusinessException("\u7528\u6237\u4e0d\u5b58\u5728", HttpStatus.NOT_FOUND))).getAppCode())) {
throw new BusinessException("\u5f53\u524d\u7ad9\u70b9\u65e0\u6743\u8bbf\u95ee\u8be5\u7528\u6237", HttpStatus.FORBIDDEN);
}
return ApiResponse.ok((Object)this.adminUserIpEnricher.toDetailRow(u));
}
@GetMapping(value={"/logs"})
public ApiResponse<Page<ClientLog>> logs(@RequestParam(required=false) Long userId, @RequestParam(required=false) String appCode, @RequestParam(required=false) String email, @RequestParam(required=false) String deviceId, @RequestParam(required=false) String level, @RequestParam(required=false, defaultValue="false") boolean crashOnly, @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="20") int size) {
PageRequest pr = PageRequest.of((int)page, (int)size, (Sort)Sort.by((Sort.Direction)Sort.Direction.DESC, (String[])new String[]{"id"}));
return ApiResponse.ok((Object)this.clientLogRepository.search(appCode == null || appCode.isBlank() ? null : appCode.trim(), userId, email == null || email.isBlank() ? null : email.trim().toLowerCase(), deviceId == null || deviceId.isBlank() ? null : deviceId.trim(), level == null || level.isBlank() ? null : level.trim(), crashOnly, (Pageable)pr));
}
@PostMapping(value={"/logs"})
public ApiResponse<ClientLog> createLog(@RequestBody ClientLog log) {
if (log.getAppCode() == null || log.getAppCode().isBlank()) {
throw new BusinessException("appCode \u4e0d\u80fd\u4e3a\u7a7a");
}
return ApiResponse.ok((Object)this.appClientService.reportLog(log.getAppCode(), log));
}
@GetMapping(value={"/versions"})
public ApiResponse<List<AppVersion>> versions(@RequestParam(required=false) String appCode) {
if (appCode != null && !appCode.isBlank()) {
return ApiResponse.ok((Object)this.versionsConfigService.findByAppCodeOrderByVersionCodeDesc(appCode));
}
return ApiResponse.ok(this.versionsConfigService.findAll().stream().sorted(Comparator.comparing(AppVersion::getId, Comparator.nullsLast(Long::compareTo)).reversed()).toList());
}
@PostMapping(value={"/versions"})
public ApiResponse<AppVersion> saveVersion(@RequestBody AppVersion v) {
WyrsAppRegistry.require((String)v.getAppCode());
AppVersion saved = this.configDbMirrorService.syncVersion(v);
this.operationLogService.log("VERSION_SAVE", "\u4fdd\u5b58\u7248\u672c " + v.getAppCode() + " " + v.getVersionName());
return ApiResponse.ok((Object)saved);
}
@DeleteMapping(value={"/versions/{id}"})
public ApiResponse<Void> deleteVersion(@PathVariable Long id) {
this.configDbMirrorService.deleteVersion(id);
this.operationLogService.log("VERSION_DELETE", "\u5220\u9664\u7248\u672c id=" + id);
return ApiResponse.ok(null);
}
@GetMapping(value={"/feedbacks"})
public ApiResponse<Page<UserFeedback>> feedbacks(@RequestParam(required=false) String appCode, @RequestParam(required=false) String status, @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="20") int size) {
PageRequest pr = PageRequest.of((int)page, (int)size, (Sort)Sort.by((Sort.Direction)Sort.Direction.DESC, (String[])new String[]{"id"}));
if (status != null && !status.isBlank()) {
return ApiResponse.ok((Object)this.userFeedbackRepository.findByStatus(status, (Pageable)pr));
}
if (appCode != null && !appCode.isBlank()) {
return ApiResponse.ok((Object)this.userFeedbackRepository.findByAppCode(appCode, (Pageable)pr));
}
return ApiResponse.ok((Object)this.userFeedbackRepository.findAll((Pageable)pr));
}
@PutMapping(value={"/feedbacks/{id}"})
public ApiResponse<UserFeedback> updateFeedback(@PathVariable Long id, @RequestBody UserFeedback body) {
UserFeedback fb = (UserFeedback)this.userFeedbackRepository.findById((Object)id).orElseThrow();
if (body.getStatus() != null) {
fb.setStatus(body.getStatus());
}
if (body.getReply() != null) {
fb.setReply(body.getReply());
}
this.operationLogService.log("FEEDBACK_REPLY", "\u5904\u7406\u53cd\u9988 id=" + id);
return ApiResponse.ok((Object)((UserFeedback)this.userFeedbackRepository.save((Object)fb)));
}
@GetMapping(value={"/banners"})
public ApiResponse<List<AppBanner>> banners(@RequestParam(required=false) String appCode) {
List<AppBanner> list = appCode != null && !appCode.isBlank() ? this.bannersConfigService.findByAppCodeOrderBySortOrderAsc(appCode) : this.bannersConfigService.findAll().stream().sorted(Comparator.comparing(AppBanner::getSortOrder, Comparator.nullsLast(Integer::compareTo))).toList();
this.bannerStateService.applyEnabledTo(list);
return ApiResponse.ok(list);
}
@PostMapping(value={"/banners"})
public ApiResponse<AppBanner> saveBanner(@RequestBody AppBanner b) {
AppBanner saved;
boolean isCreate;
WyrsAppRegistry.require((String)b.getAppCode());
Boolean requestedEnabled = b.getEnabled();
boolean bl = isCreate = b.getId() == null;
if (!isCreate) {
AppBanner existing = (AppBanner)this.bannersConfigService.findById(b.getId()).orElseThrow();
existing.setTitle(b.getTitle());
existing.setImageUrl(b.getImageUrl());
existing.setLinkUrl(b.getLinkUrl());
existing.setSortOrder(b.getSortOrder());
existing.setStartTime(b.getStartTime());
existing.setEndTime(b.getEndTime());
if (requestedEnabled != null) {
existing.setEnabled(requestedEnabled);
}
saved = this.configDbMirrorService.syncBannerToFile(existing);
} else {
if (requestedEnabled == null) {
b.setEnabled(Boolean.valueOf(false));
}
saved = this.configDbMirrorService.syncBannerToFile(b);
}
if (saved.getId() != null && (isCreate || requestedEnabled != null)) {
boolean enabled = requestedEnabled != null ? requestedEnabled.booleanValue() : Boolean.TRUE.equals(saved.getEnabled());
this.bannerStateService.setEnabled(saved.getAppCode(), saved.getId().longValue(), enabled);
}
this.bannerStateService.applyEnabledTo(List.of(saved));
this.operationLogService.log("BANNER_SAVE", "\u4fdd\u5b58Banner " + saved.getTitle());
return ApiResponse.ok((Object)saved);
}
@PatchMapping(value={"/banners/{id}/enabled"})
public ApiResponse<AppBanner> setBannerEnabled(@PathVariable Long id, @RequestBody Map<String, Boolean> body) {
AppBanner banner = (AppBanner)this.bannersConfigService.findById(id).orElseThrow();
Boolean enabled = body.get("enabled");
if (enabled == null) {
throw new BusinessException("enabled \u4e0d\u80fd\u4e3a\u7a7a");
}
this.bannerStateService.setEnabled(banner.getAppCode(), id.longValue(), enabled.booleanValue());
banner.setEnabled(enabled);
this.configDbMirrorService.syncBannerToFile(banner);
this.operationLogService.log("BANNER_TOGGLE", "Banner id=" + id + " enabled=" + enabled);
return ApiResponse.ok((Object)banner);
}
@DeleteMapping(value={"/banners/{id}"})
public ApiResponse<Void> deleteBanner(@PathVariable Long id) {
this.configDbMirrorService.deleteBanner(id);
this.operationLogService.log("BANNER_DELETE", "\u5220\u9664Banner id=" + id);
return ApiResponse.ok(null);
}
@PostMapping(value={"/banners/{id}/image"})
public ApiResponse<Map<String, Object>> uploadBannerImage(@PathVariable Long id, @RequestParam(value="file") MultipartFile file) throws IOException {
return ApiResponse.ok((Object)this.bannerImageService.uploadImage(id, file));
}
@GetMapping(value={"/prices"})
public ApiResponse<List<MembershipPrice>> prices(@RequestParam(required=false) String appCode) {
if (appCode != null && !appCode.isBlank()) {
return ApiResponse.ok((Object)this.pricesConfigService.findByAppCode(appCode));
}
return ApiResponse.ok((Object)this.pricesConfigService.findAll());
}
@PostMapping(value={"/prices"})
public ApiResponse<MembershipPrice> savePrice(@RequestBody MembershipPrice p) {
WyrsAppRegistry.require((String)p.getAppCode());
MembershipPrice saved = this.configDbMirrorService.syncPrice(p);
this.operationLogService.log("PRICE_SAVE", "\u4fdd\u5b58\u4f1a\u5458\u4ef7\u683c " + p.getName());
return ApiResponse.ok((Object)saved);
}
@DeleteMapping(value={"/prices/{id}"})
public ApiResponse<Void> deletePrice(@PathVariable Long id) {
this.configDbMirrorService.deletePrice(id);
return ApiResponse.ok(null);
}
@GetMapping(value={"/payment-channels/catalog"})
public ApiResponse<Map<String, Object>> paymentChannelCatalog() {
return ApiResponse.ok((Object)this.paymentChannelConfigService.channelCatalog());
}
@GetMapping(value={"/payment-channels"})
public ApiResponse<List<Map<String, Object>>> paymentChannels(@RequestParam(required=false) String appCode) {
String scoped = this.resolveScopedAppCode(appCode);
return ApiResponse.ok((Object)this.paymentChannelConfigService.listMasked(scoped));
}
@PostMapping(value={"/payment-channels"})
public ApiResponse<Map<String, Object>> savePaymentChannel(@RequestBody PaymentChannelConfig body) {
if (body.getAppCode() == null || body.getAppCode().isBlank()) {
body.setAppCode(this.resolveScopedAppCode(null));
}
WyrsAppRegistry.require((String)body.getAppCode());
PaymentChannelConfig saved = this.paymentChannelConfigService.saveChannel(body);
this.operationLogService.log("PAYMENT_CHANNEL_SAVE", "\u4fdd\u5b58\u652f\u4ed8\u6e20\u9053 " + saved.getAppCode() + "/" + saved.getChannel());
return ApiResponse.ok((Object)this.paymentChannelConfigService.toMaskedMap(saved));
}
@DeleteMapping(value={"/payment-channels/{id}"})
public ApiResponse<Void> deletePaymentChannel(@PathVariable Long id) {
this.paymentChannelConfigService.deleteChannel(id);
this.operationLogService.log("PAYMENT_CHANNEL_DELETE", "\u5220\u9664\u652f\u4ed8\u6e20\u9053 id=" + id);
return ApiResponse.ok(null);
}
private String resolveScopedAppCode(String appCode) {
if (appCode != null && !appCode.isBlank()) {
return appCode.trim();
}
AdminSiteScope scope = AdminSiteScope.current();
if (scope != null && scope.kind() == AdminSiteScope.Kind.POPADMIN) {
return "spark";
}
return null;
}
@GetMapping(value={"/developer-apps"})
public ApiResponse<List<DeveloperApp>> developerApps(@RequestParam(required=false) String appCode) {
if (appCode != null && !appCode.isBlank()) {
return ApiResponse.ok((Object)this.developerAppsConfigService.findBySourceAppCodeOrderBySortOrder(appCode));
}
return ApiResponse.ok((Object)this.developerAppsConfigService.findAll());
}
@PostMapping(value={"/developer-apps"})
public ApiResponse<DeveloperApp> saveDeveloperApp(@RequestBody DeveloperApp app) {
WyrsAppRegistry.require((String)app.getSourceAppCode());
DeveloperApp saved = this.developerAppsConfigService.save(app);
this.operationLogService.log("DEV_APP_SAVE", "\u4fdd\u5b58\u540c\u5f00\u53d1\u8005\u5e94\u7528 " + saved.getTargetAppName());
return ApiResponse.ok((Object)saved);
}
@PatchMapping(value={"/developer-apps/{id}/enabled"})
public ApiResponse<DeveloperApp> setDeveloperAppEnabled(@PathVariable Long id, @RequestBody Map<String, Boolean> body) {
DeveloperApp app = (DeveloperApp)this.developerAppsConfigService.findById(id).orElseThrow();
Boolean enabled = body.get("enabled");
if (enabled == null) {
throw new BusinessException("enabled \u4e0d\u80fd\u4e3a\u7a7a");
}
app.setEnabled(enabled);
DeveloperApp saved = this.developerAppsConfigService.save(app);
this.operationLogService.log("DEV_APP_TOGGLE", "\u540c\u5f00\u53d1\u8005\u5e94\u7528 id=" + id + " enabled=" + enabled);
return ApiResponse.ok((Object)saved);
}
@DeleteMapping(value={"/developer-apps/{id}"})
public ApiResponse<Void> deleteDeveloperApp(@PathVariable Long id) {
this.developerAppsConfigService.deleteById(id);
this.operationLogService.log("DEV_APP_DELETE", "\u5220\u9664\u540c\u5f00\u53d1\u8005\u5e94\u7528 id=" + id);
return ApiResponse.ok(null);
}
@PostMapping(value={"/developer-apps/sync-website-icons"})
public ApiResponse<Map<String, Object>> syncDeveloperAppWebsiteIcons() {
int updated = this.developerAppsConfigService.syncAllWebsiteIcons();
this.operationLogService.log("DEV_APP_ICON_SYNC", "\u540c\u6b65\u5b98\u7f51\u56fe\u6807 updated=" + updated);
return ApiResponse.ok(Map.of("updated", updated));
}
@GetMapping(value={"/ad-placements"})
public ApiResponse<List<AppAdPlacement>> adPlacements(@RequestParam(required=false) String appCode) {
if (appCode != null && !appCode.isBlank()) {
return ApiResponse.ok(this.adPlacementsConfigService.findOneByAppCode(appCode).map(List::of).orElse(List.of()));
}
return ApiResponse.ok(this.adPlacementsConfigService.findAll().stream().sorted(Comparator.comparing(AppAdPlacement::getAppCode)).toList());
}
@PutMapping(value={"/ad-placements/{appCode}"})
public ApiResponse<AppAdPlacement> saveAdPlacements(@PathVariable String appCode, @RequestBody AppAdPlacement body) {
WyrsAppRegistry.require((String)appCode);
body.setAppCode(appCode);
AppAdPlacement saved = this.adPlacementsConfigService.upsertByAppCode(body);
this.operationLogService.log("AD_PLACEMENT_SAVE", "\u4fdd\u5b58\u5e7f\u544a\u4f4d\u914d\u7f6e " + appCode);
return ApiResponse.ok((Object)saved);
}
@GetMapping(value={"/apk-downloads"})
public ApiResponse<List<AppApkDownload>> apkDownloads(@RequestParam(required=false) String appCode) {
if (appCode != null && !appCode.isBlank()) {
return ApiResponse.ok(this.apkDownloadsConfigService.findOneByAppCode(appCode).map(List::of).orElse(List.of()));
}
return ApiResponse.ok(this.apkDownloadsConfigService.findAll().stream().sorted(Comparator.comparing(AppApkDownload::getAppCode)).toList());
}
@PutMapping(value={"/apk-downloads/{appCode}"})
public ApiResponse<AppApkDownload> saveApkDownload(@PathVariable String appCode, @RequestBody AppApkDownload body) {
WyrsAppRegistry.require((String)appCode);
body.setAppCode(appCode);
AppApkDownload saved = this.apkDownloadsConfigService.upsertByAppCode(body);
this.operationLogService.log("APK_DOWNLOAD_SAVE", "\u4fdd\u5b58 APK/\u5e94\u7528\u5e02\u573a\u914d\u7f6e " + appCode);
return ApiResponse.ok((Object)saved);
}
@PostMapping(value={"/apk-downloads/{appCode}/apk"})
public ApiResponse<Map<String, Object>> uploadReleaseApk(@PathVariable String appCode, @RequestParam(value="file") MultipartFile file) throws IOException {
WyrsAppRegistry.require((String)appCode);
return ApiResponse.ok((Object)this.apkUploadService.uploadReleaseApk(appCode, file));
}
@PostMapping(value={"/apk-downloads/{appCode}/windows"})
public ApiResponse<Map<String, Object>> uploadWindowsPackage(@PathVariable String appCode, @RequestParam(value="file") MultipartFile file) throws IOException {
WyrsAppRegistry.require((String)appCode);
return ApiResponse.ok((Object)this.apkUploadService.uploadWindowsPackage(appCode, file));
}
@GetMapping(value={"/site-content"})
public ApiResponse<List<AppSiteContent>> siteContentList(@RequestParam(required=false) String appCode) {
if (appCode != null && !appCode.isBlank()) {
return ApiResponse.ok(List.of(this.siteContentConfigService.getOrDefault(appCode)));
}
return ApiResponse.ok((Object)this.siteContentConfigService.listAll());
}
@PutMapping(value={"/site-content/{appCode}"})
public ApiResponse<AppSiteContent> saveSiteContent(@PathVariable String appCode, @RequestBody AppSiteContent body) {
WyrsAppRegistry.require((String)appCode);
AppSiteContent saved = this.siteContentConfigService.save(appCode, body);
AppApkDownload apk = this.apkDownloadsConfigService.findOneByAppCode(appCode).orElseGet(AppApkDownload::new);
apk.setAppCode(appCode);
if (body.getDownloadUrl() != null) {
apk.setWindowsUrl(body.getDownloadUrl());
}
if (body.getDownloadOnShelf() != null) {
apk.setWindowsOnShelf(body.getDownloadOnShelf());
}
this.apkDownloadsConfigService.upsertByAppCode(apk);
this.operationLogService.log("SITE_CONTENT_SAVE", "\u4fdd\u5b58\u5b98\u7f51\u5185\u5bb9 " + appCode);
return ApiResponse.ok((Object)saved);
}
@PostMapping(value={"/site-content/{appCode}/logo"})
public ApiResponse<AppSiteContent> uploadSiteLogo(@PathVariable String appCode, @RequestParam(value="file") MultipartFile file) throws IOException {
WyrsAppRegistry.require((String)appCode);
AppSiteContent saved = this.siteContentConfigService.saveLogo(appCode, file);
this.operationLogService.log("SITE_CONTENT_LOGO", "\u4e0a\u4f20\u5b98\u7f51 Logo " + appCode);
return ApiResponse.ok((Object)saved);
}
@GetMapping(value={"/whitelist"})
public ApiResponse<List<AppWhitelistEntry>> whitelist(@RequestParam(required=false) String appCode, @RequestParam(required=false) String type) {
return ApiResponse.ok((Object)this.appWhitelistConfigService.list(appCode, type));
}
@PostMapping(value={"/whitelist"})
public ApiResponse<AppWhitelistEntry> createWhitelist(@RequestBody AppWhitelistEntry body) {
AppWhitelistEntry saved = this.appWhitelistConfigService.create(body);
this.operationLogService.log("WHITELIST_ADD", "\u6dfb\u52a0\u767d\u540d\u5355 " + saved.getAppCode() + " " + saved.getType() + "=" + saved.getValue());
return ApiResponse.ok((Object)saved);
}
@PutMapping(value={"/whitelist/{id}"})
public ApiResponse<AppWhitelistEntry> updateWhitelist(@PathVariable Long id, @RequestBody AppWhitelistEntry body) {
AppWhitelistEntry saved = this.appWhitelistConfigService.update(id, body);
this.operationLogService.log("WHITELIST_UPDATE", "\u66f4\u65b0\u767d\u540d\u5355 id=" + id + " " + saved.getType() + "=" + saved.getValue());
return ApiResponse.ok((Object)saved);
}
@PatchMapping(value={"/whitelist/{id}/enabled"})
public ApiResponse<AppWhitelistEntry> setWhitelistEnabled(@PathVariable Long id, @RequestBody Map<String, Boolean> body) {
Boolean enabled = body.get("enabled");
if (enabled == null) {
throw new BusinessException("enabled \u4e0d\u80fd\u4e3a\u7a7a");
}
AppWhitelistEntry saved = this.appWhitelistConfigService.setEnabled(id, enabled.booleanValue());
this.operationLogService.log("WHITELIST_TOGGLE", "\u767d\u540d\u5355 id=" + id + " enabled=" + enabled);
return ApiResponse.ok((Object)saved);
}
@DeleteMapping(value={"/whitelist/{id}"})
public ApiResponse<Void> deleteWhitelist(@PathVariable Long id) {
this.appWhitelistConfigService.delete(id);
this.operationLogService.log("WHITELIST_DELETE", "\u5220\u9664\u767d\u540d\u5355 id=" + id);
return ApiResponse.ok(null);
}
@GetMapping(value={"/app-features/{appCode}"})
public ApiResponse<AppClientFeature> getAppFeatures(@PathVariable String appCode) {
return ApiResponse.ok((Object)this.appClientFeatureConfigService.getOrDefault(appCode));
}
@PutMapping(value={"/app-features/{appCode}"})
public ApiResponse<AppClientFeature> saveAppFeatures(@PathVariable String appCode, @RequestBody Map<String, Object> body) {
boolean bl;
Object raw = body.get("familySharingEnabled");
if (raw == null) {
throw new BusinessException("familySharingEnabled \u4e0d\u80fd\u4e3a\u7a7a");
}
if (raw instanceof Boolean) {
Boolean b = (Boolean)raw;
bl = b;
} else {
bl = Boolean.parseBoolean(String.valueOf(raw));
}
boolean enabled = bl;
AppClientFeature saved = this.appClientFeatureConfigService.upsert(appCode, Boolean.valueOf(enabled));
this.operationLogService.log("APP_FEATURE_SAVE", "\u4fdd\u5b58\u529f\u80fd\u5f00\u5173 " + appCode + " familySharingEnabled=" + enabled);
return ApiResponse.ok((Object)saved);
}
@PostMapping(value={"/developer-apps/{id}/icon"})
public ApiResponse<Map<String, Object>> uploadDeveloperAppIcon(@PathVariable Long id, @RequestParam(value="file") MultipartFile file) throws IOException {
return ApiResponse.ok((Object)this.developerAppIconService.uploadIcon(id, file));
}
@GetMapping(value={"/stats/dau"})
public ApiResponse<List<DailyActiveStat>> dau(@RequestParam(required=false) String appCode, @RequestParam(required=false) String start, @RequestParam(required=false) String end) {
LocalDate e;
LocalDate s = start != null ? LocalDate.parse(start) : AdminStatsService.todayInChina().minusDays(30L);
LocalDate localDate = e = end != null ? LocalDate.parse(end) : AdminStatsService.todayInChina();
if (appCode != null && !appCode.isBlank()) {
return ApiResponse.ok((Object)this.dailyActiveStatRepository.findByAppCodeAndStatDateBetweenOrderByStatDateAsc(appCode, s, e));
}
return ApiResponse.ok((Object)this.dailyActiveStatRepository.findByStatDateBetweenOrderByStatDateDesc(s, e));
}
@GetMapping(value={"/stats/dau/channels"})
public ApiResponse<List<Map<String, Object>>> channelDau(@RequestParam String appCode, @RequestParam(required=false) String start, @RequestParam(required=false) String end) {
WyrsAppRegistry.require((String)appCode);
LocalDate s = start != null ? LocalDate.parse(start) : AdminStatsService.todayInChina().minusDays(7L);
LocalDate e = end != null ? LocalDate.parse(end) : AdminStatsService.todayInChina();
return ApiResponse.ok((Object)this.adminStatsService.channelDau(appCode, s, e));
}
@GetMapping(value={"/stats/dau/platforms"})
public ApiResponse<List<Map<String, Object>>> platformDau(@RequestParam String appCode, @RequestParam(required=false) String start, @RequestParam(required=false) String end) {
WyrsAppRegistry.require((String)appCode);
LocalDate s = start != null ? LocalDate.parse(start) : AdminStatsService.todayInChina().minusDays(7L);
LocalDate e = end != null ? LocalDate.parse(end) : AdminStatsService.todayInChina();
return ApiResponse.ok((Object)this.adminStatsService.platformDau(appCode, s, e));
}
@GetMapping(value={"/stats/dau/countries"})
public ApiResponse<List<Map<String, Object>>> countryDau(@RequestParam String appCode, @RequestParam(required=false) String date) {
WyrsAppRegistry.require((String)appCode);
LocalDate d = date != null ? LocalDate.parse(date) : AdminStatsService.todayInChina();
return ApiResponse.ok((Object)this.adminStatsService.countryDau(appCode, d));
}
@GetMapping(value={"/stats/retention"})
public ApiResponse<List<Map<String, Object>>> retention(@RequestParam String appCode, @RequestParam(required=false) String channel, @RequestParam(defaultValue="7") int cohortDays) {
WyrsAppRegistry.require((String)appCode);
return ApiResponse.ok((Object)this.adminStatsService.retentionStats(appCode, channel, cohortDays));
}
@GetMapping(value={"/stats/share"})
public ApiResponse<Map<String, Object>> shareStats(@RequestParam String appCode, @RequestParam(required=false) String start, @RequestParam(required=false) String end) {
WyrsAppRegistry.require((String)appCode);
LocalDate s = start != null ? LocalDate.parse(start) : null;
LocalDate e = end != null ? LocalDate.parse(end) : null;
return ApiResponse.ok((Object)this.adminStatsService.shareStats(appCode, s, e));
}
@PostMapping(value={"/stats/dau"})
public ApiResponse<DailyActiveStat> saveDau(@RequestBody DailyActiveStat stat) {
return ApiResponse.ok((Object)((DailyActiveStat)this.dailyActiveStatRepository.save((Object)stat)));
}
@PostMapping(value={"/dau/purge-mock"})
public ApiResponse<Map<String, Object>> purgeMockDau(@RequestParam(required=false) String from, @RequestParam(required=false) String to) {
LocalDate fromDate = from != null ? LocalDate.parse(from) : null;
LocalDate toDate = to != null ? LocalDate.parse(to) : null;
Map result = this.dauCleanupService.purgeMockUsersAndRecalculate(fromDate, toDate);
this.operationLogService.log("DAU_PURGE_MOCK", "\u6e05\u7406\u6a21\u62df\u65e5\u6d3b deleted=" + String.valueOf(result.get("deletedCount")) + " todayDau=" + String.valueOf(result.get("todayDauByApp")));
return ApiResponse.ok((Object)result);
}
@PostMapping(value={"/dau/recalculate"})
public ApiResponse<Map<String, Object>> recalculateDau(@RequestParam(required=false) String from, @RequestParam(required=false) String to) {
LocalDate fromDate = from != null ? LocalDate.parse(from) : null;
LocalDate toDate = to != null ? LocalDate.parse(to) : null;
Map result = this.dauCleanupService.recalculateDau(fromDate, toDate);
this.operationLogService.log("DAU_RECALCULATE", "\u91cd\u7b97\u65e5\u6d3b from=" + String.valueOf(result.get("recalcFrom")) + " to=" + String.valueOf(result.get("recalcTo")) + " todayDau=" + String.valueOf(result.get("todayDauByApp")));
return ApiResponse.ok((Object)result);
}
@GetMapping(value={"/payments"})
public ApiResponse<Page<PaymentRecord>> payments(@RequestParam(required=false) String appCode, @RequestParam(required=false) Long userId, @RequestParam(required=false) String period, @RequestParam(required=false) String start, @RequestParam(required=false) String end, @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="20") int size) {
PageRequest pr = PageRequest.of((int)page, (int)size);
AdminStatsService.DateRange range = this.adminStatsService.resolveRange(period, start != null ? LocalDate.parse(start) : null, end != null ? LocalDate.parse(end) : null);
LocalDateTime s = range.start().atStartOfDay();
LocalDateTime e = range.endExclusive().atStartOfDay();
String code = appCode == null || appCode.isBlank() ? null : appCode;
return ApiResponse.ok((Object)this.paymentRecordRepository.findByFilters(code, userId, s, e, (Pageable)pr));
}
@GetMapping(value={"/payments/summary"})
public ApiResponse<Map<String, Object>> paymentSummary(@RequestParam(required=false) String appCode, @RequestParam(defaultValue="month") String period, @RequestParam(required=false) String start, @RequestParam(required=false) String end) {
LocalDate s = start != null ? LocalDate.parse(start) : null;
LocalDate e = end != null ? LocalDate.parse(end) : null;
String code = appCode == null || appCode.isBlank() ? null : appCode;
return ApiResponse.ok((Object)this.adminStatsService.paymentSummary(code, period, s, e));
}
@GetMapping(value={"/operation-logs"})
public ApiResponse<Page<AdminOperationLog>> operationLogs(@RequestParam(required=false) String action, @RequestParam(defaultValue="0") int page, @RequestParam(defaultValue="20") int size) {
return ApiResponse.ok((Object)this.operationLogService.list(action, (Pageable)PageRequest.of((int)page, (int)size, (Sort)Sort.by((Sort.Direction)Sort.Direction.DESC, (String[])new String[]{"id"}))));
}
public AdminManageController(AppUserRepository appUserRepository, ClientLogRepository clientLogRepository, UserFeedbackRepository userFeedbackRepository, PaymentRecordRepository paymentRecordRepository, DailyActiveStatRepository dailyActiveStatRepository, OperationLogService operationLogService, AdminStatsService adminStatsService, ServerMonitorService serverMonitorService, JavaPageMetricsService javaPageMetricsService, CapacityPlanService capacityPlanService, DauCleanupService dauCleanupService, BannerImageService bannerImageService, BannerStateService bannerStateService, BannersConfigService bannersConfigService, VersionsConfigService versionsConfigService, PricesConfigService pricesConfigService, PaymentChannelConfigService paymentChannelConfigService, DeveloperAppsConfigService developerAppsConfigService, AdPlacementsConfigService adPlacementsConfigService, ApkDownloadsConfigService apkDownloadsConfigService, AppSiteContentConfigService siteContentConfigService, AppWhitelistConfigService appWhitelistConfigService, AppClientFeatureConfigService appClientFeatureConfigService, ApkUploadService apkUploadService, DeveloperAppIconService developerAppIconService, AdminConfigDbMirrorService configDbMirrorService, AppUserLookupService appUserLookupService, AppClientService appClientService, OnlineDeviceTracker onlineDeviceTracker, AdminUserIpEnricher adminUserIpEnricher) {
this.appUserRepository = appUserRepository;
this.clientLogRepository = clientLogRepository;
this.userFeedbackRepository = userFeedbackRepository;
this.paymentRecordRepository = paymentRecordRepository;
this.dailyActiveStatRepository = dailyActiveStatRepository;
this.operationLogService = operationLogService;
this.adminStatsService = adminStatsService;
this.serverMonitorService = serverMonitorService;
this.javaPageMetricsService = javaPageMetricsService;
this.capacityPlanService = capacityPlanService;
this.dauCleanupService = dauCleanupService;
this.bannerImageService = bannerImageService;
this.bannerStateService = bannerStateService;
this.bannersConfigService = bannersConfigService;
this.versionsConfigService = versionsConfigService;
this.pricesConfigService = pricesConfigService;
this.paymentChannelConfigService = paymentChannelConfigService;
this.developerAppsConfigService = developerAppsConfigService;
this.adPlacementsConfigService = adPlacementsConfigService;
this.apkDownloadsConfigService = apkDownloadsConfigService;
this.siteContentConfigService = siteContentConfigService;
this.appWhitelistConfigService = appWhitelistConfigService;
this.appClientFeatureConfigService = appClientFeatureConfigService;
this.apkUploadService = apkUploadService;
this.developerAppIconService = developerAppIconService;
this.configDbMirrorService = configDbMirrorService;
this.appUserLookupService = appUserLookupService;
this.appClientService = appClientService;
this.onlineDeviceTracker = onlineDeviceTracker;
this.adminUserIpEnricher = adminUserIpEnricher;
}
}