代理活动修复
This commit is contained in:
parent
c93a30f3db
commit
a0c9d055f0
File diff suppressed because it is too large
Load Diff
@ -2191,6 +2191,10 @@ message AgencyOpeningApplication {
|
||||
int64 granted_at_ms = 16;
|
||||
int64 updated_at_ms = 17;
|
||||
int64 reward_threshold_coin_spent = 18;
|
||||
int64 approved_at_ms = 19;
|
||||
int64 score_start_ms = 20;
|
||||
int64 score_end_ms = 21;
|
||||
int64 reviewed_by_admin_id = 22;
|
||||
}
|
||||
|
||||
// AgencyOpeningAgencySnapshot 是申请页展示和资格判断使用的代理快照。
|
||||
@ -2263,6 +2267,17 @@ message ListAgencyOpeningApplicationsResponse {
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message ReviewAgencyOpeningApplicationRequest {
|
||||
RequestMeta meta = 1;
|
||||
string application_id = 2;
|
||||
string action = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message ReviewAgencyOpeningApplicationResponse {
|
||||
AgencyOpeningApplication application = 1;
|
||||
}
|
||||
|
||||
message GetAgencyOpeningStatusRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
@ -2483,6 +2498,7 @@ service AdminAgencyOpeningService {
|
||||
rpc UpdateAgencyOpeningCycle(UpsertAgencyOpeningCycleRequest) returns (UpsertAgencyOpeningCycleResponse);
|
||||
rpc SetAgencyOpeningCycleStatus(SetAgencyOpeningCycleStatusRequest) returns (SetAgencyOpeningCycleStatusResponse);
|
||||
rpc ListAgencyOpeningApplications(ListAgencyOpeningApplicationsRequest) returns (ListAgencyOpeningApplicationsResponse);
|
||||
rpc ReviewAgencyOpeningApplication(ReviewAgencyOpeningApplicationRequest) returns (ReviewAgencyOpeningApplicationResponse);
|
||||
}
|
||||
|
||||
// SevenDayCheckInService 拥有 App 七日签到查询和签到命令。
|
||||
|
||||
@ -4962,12 +4962,13 @@ var AdminCPWeeklyRankService_ServiceDesc = grpc.ServiceDesc{
|
||||
}
|
||||
|
||||
const (
|
||||
AdminAgencyOpeningService_ListAgencyOpeningCycles_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/ListAgencyOpeningCycles"
|
||||
AdminAgencyOpeningService_CreateAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/CreateAgencyOpeningCycle"
|
||||
AdminAgencyOpeningService_GetAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/GetAgencyOpeningCycle"
|
||||
AdminAgencyOpeningService_UpdateAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/UpdateAgencyOpeningCycle"
|
||||
AdminAgencyOpeningService_SetAgencyOpeningCycleStatus_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/SetAgencyOpeningCycleStatus"
|
||||
AdminAgencyOpeningService_ListAgencyOpeningApplications_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/ListAgencyOpeningApplications"
|
||||
AdminAgencyOpeningService_ListAgencyOpeningCycles_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/ListAgencyOpeningCycles"
|
||||
AdminAgencyOpeningService_CreateAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/CreateAgencyOpeningCycle"
|
||||
AdminAgencyOpeningService_GetAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/GetAgencyOpeningCycle"
|
||||
AdminAgencyOpeningService_UpdateAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/UpdateAgencyOpeningCycle"
|
||||
AdminAgencyOpeningService_SetAgencyOpeningCycleStatus_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/SetAgencyOpeningCycleStatus"
|
||||
AdminAgencyOpeningService_ListAgencyOpeningApplications_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/ListAgencyOpeningApplications"
|
||||
AdminAgencyOpeningService_ReviewAgencyOpeningApplication_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/ReviewAgencyOpeningApplication"
|
||||
)
|
||||
|
||||
// AdminAgencyOpeningServiceClient is the client API for AdminAgencyOpeningService service.
|
||||
@ -4982,6 +4983,7 @@ type AdminAgencyOpeningServiceClient interface {
|
||||
UpdateAgencyOpeningCycle(ctx context.Context, in *UpsertAgencyOpeningCycleRequest, opts ...grpc.CallOption) (*UpsertAgencyOpeningCycleResponse, error)
|
||||
SetAgencyOpeningCycleStatus(ctx context.Context, in *SetAgencyOpeningCycleStatusRequest, opts ...grpc.CallOption) (*SetAgencyOpeningCycleStatusResponse, error)
|
||||
ListAgencyOpeningApplications(ctx context.Context, in *ListAgencyOpeningApplicationsRequest, opts ...grpc.CallOption) (*ListAgencyOpeningApplicationsResponse, error)
|
||||
ReviewAgencyOpeningApplication(ctx context.Context, in *ReviewAgencyOpeningApplicationRequest, opts ...grpc.CallOption) (*ReviewAgencyOpeningApplicationResponse, error)
|
||||
}
|
||||
|
||||
type adminAgencyOpeningServiceClient struct {
|
||||
@ -5052,6 +5054,16 @@ func (c *adminAgencyOpeningServiceClient) ListAgencyOpeningApplications(ctx cont
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminAgencyOpeningServiceClient) ReviewAgencyOpeningApplication(ctx context.Context, in *ReviewAgencyOpeningApplicationRequest, opts ...grpc.CallOption) (*ReviewAgencyOpeningApplicationResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReviewAgencyOpeningApplicationResponse)
|
||||
err := c.cc.Invoke(ctx, AdminAgencyOpeningService_ReviewAgencyOpeningApplication_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AdminAgencyOpeningServiceServer is the server API for AdminAgencyOpeningService service.
|
||||
// All implementations must embed UnimplementedAdminAgencyOpeningServiceServer
|
||||
// for forward compatibility.
|
||||
@ -5064,6 +5076,7 @@ type AdminAgencyOpeningServiceServer interface {
|
||||
UpdateAgencyOpeningCycle(context.Context, *UpsertAgencyOpeningCycleRequest) (*UpsertAgencyOpeningCycleResponse, error)
|
||||
SetAgencyOpeningCycleStatus(context.Context, *SetAgencyOpeningCycleStatusRequest) (*SetAgencyOpeningCycleStatusResponse, error)
|
||||
ListAgencyOpeningApplications(context.Context, *ListAgencyOpeningApplicationsRequest) (*ListAgencyOpeningApplicationsResponse, error)
|
||||
ReviewAgencyOpeningApplication(context.Context, *ReviewAgencyOpeningApplicationRequest) (*ReviewAgencyOpeningApplicationResponse, error)
|
||||
mustEmbedUnimplementedAdminAgencyOpeningServiceServer()
|
||||
}
|
||||
|
||||
@ -5092,6 +5105,9 @@ func (UnimplementedAdminAgencyOpeningServiceServer) SetAgencyOpeningCycleStatus(
|
||||
func (UnimplementedAdminAgencyOpeningServiceServer) ListAgencyOpeningApplications(context.Context, *ListAgencyOpeningApplicationsRequest) (*ListAgencyOpeningApplicationsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAgencyOpeningApplications not implemented")
|
||||
}
|
||||
func (UnimplementedAdminAgencyOpeningServiceServer) ReviewAgencyOpeningApplication(context.Context, *ReviewAgencyOpeningApplicationRequest) (*ReviewAgencyOpeningApplicationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReviewAgencyOpeningApplication not implemented")
|
||||
}
|
||||
func (UnimplementedAdminAgencyOpeningServiceServer) mustEmbedUnimplementedAdminAgencyOpeningServiceServer() {
|
||||
}
|
||||
func (UnimplementedAdminAgencyOpeningServiceServer) testEmbeddedByValue() {}
|
||||
@ -5222,6 +5238,24 @@ func _AdminAgencyOpeningService_ListAgencyOpeningApplications_Handler(srv interf
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminAgencyOpeningService_ReviewAgencyOpeningApplication_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReviewAgencyOpeningApplicationRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminAgencyOpeningServiceServer).ReviewAgencyOpeningApplication(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminAgencyOpeningService_ReviewAgencyOpeningApplication_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminAgencyOpeningServiceServer).ReviewAgencyOpeningApplication(ctx, req.(*ReviewAgencyOpeningApplicationRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AdminAgencyOpeningService_ServiceDesc is the grpc.ServiceDesc for AdminAgencyOpeningService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -5253,6 +5287,10 @@ var AdminAgencyOpeningService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListAgencyOpeningApplications",
|
||||
Handler: _AdminAgencyOpeningService_ListAgencyOpeningApplications_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReviewAgencyOpeningApplication",
|
||||
Handler: _AdminAgencyOpeningService_ReviewAgencyOpeningApplication_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
|
||||
@ -48,6 +48,7 @@ type Client interface {
|
||||
UpdateAgencyOpeningCycle(ctx context.Context, req *activityv1.UpsertAgencyOpeningCycleRequest) (*activityv1.UpsertAgencyOpeningCycleResponse, error)
|
||||
SetAgencyOpeningCycleStatus(ctx context.Context, req *activityv1.SetAgencyOpeningCycleStatusRequest) (*activityv1.SetAgencyOpeningCycleStatusResponse, error)
|
||||
ListAgencyOpeningApplications(ctx context.Context, req *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, error)
|
||||
ReviewAgencyOpeningApplication(ctx context.Context, req *activityv1.ReviewAgencyOpeningApplicationRequest) (*activityv1.ReviewAgencyOpeningApplicationResponse, error)
|
||||
GetCPWeeklyRankConfig(ctx context.Context, req *activityv1.GetCPWeeklyRankConfigRequest) (*activityv1.GetCPWeeklyRankConfigResponse, error)
|
||||
UpdateCPWeeklyRankConfig(ctx context.Context, req *activityv1.UpdateCPWeeklyRankConfigRequest) (*activityv1.UpdateCPWeeklyRankConfigResponse, error)
|
||||
ListCPWeeklyRankSettlements(ctx context.Context, req *activityv1.ListCPWeeklyRankSettlementsRequest) (*activityv1.ListCPWeeklyRankSettlementsResponse, error)
|
||||
@ -258,6 +259,10 @@ func (c *GRPCClient) ListAgencyOpeningApplications(ctx context.Context, req *act
|
||||
return c.agencyOpeningClient.ListAgencyOpeningApplications(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ReviewAgencyOpeningApplication(ctx context.Context, req *activityv1.ReviewAgencyOpeningApplicationRequest) (*activityv1.ReviewAgencyOpeningApplicationResponse, error) {
|
||||
return c.agencyOpeningClient.ReviewAgencyOpeningApplication(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetCPWeeklyRankConfig(ctx context.Context, req *activityv1.GetCPWeeklyRankConfigRequest) (*activityv1.GetCPWeeklyRankConfigResponse, error) {
|
||||
return c.cpWeeklyRankClient.GetCPWeeklyRankConfig(ctx, req)
|
||||
}
|
||||
|
||||
@ -81,6 +81,10 @@ type applicationDTO struct {
|
||||
WalletTransactionID string `json:"wallet_transaction_id"`
|
||||
FailureReason string `json:"failure_reason"`
|
||||
AppliedAtMS int64 `json:"applied_at_ms"`
|
||||
ApprovedAtMS int64 `json:"approved_at_ms"`
|
||||
ScoreStartMS int64 `json:"score_start_ms"`
|
||||
ScoreEndMS int64 `json:"score_end_ms"`
|
||||
ReviewedByAdminID int64 `json:"reviewed_by_admin_id"`
|
||||
GrantedAtMS int64 `json:"granted_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
@ -201,6 +205,23 @@ func (h *Handler) ListApplications(c *gin.Context) {
|
||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||
}
|
||||
|
||||
func (h *Handler) ApproveApplication(c *gin.Context) {
|
||||
applicationID := strings.TrimSpace(c.Param("application_id"))
|
||||
resp, err := h.activity.ReviewAgencyOpeningApplication(c.Request.Context(), &activityv1.ReviewAgencyOpeningApplicationRequest{
|
||||
Meta: h.meta(c),
|
||||
ApplicationId: applicationID,
|
||||
Action: "approve",
|
||||
OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
item := applicationFromProto(resp.GetApplication())
|
||||
shared.OperationLogWithResourceID(c, h.audit, "approve-agency-opening-application", "agency_opening_applications", item.ApplicationID, "success", "")
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
|
||||
return &activityv1.RequestMeta{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
@ -288,6 +309,10 @@ func applicationFromProto(item *activityv1.AgencyOpeningApplication) application
|
||||
WalletTransactionID: item.GetWalletTransactionId(),
|
||||
FailureReason: item.GetFailureReason(),
|
||||
AppliedAtMS: item.GetAppliedAtMs(),
|
||||
ApprovedAtMS: item.GetApprovedAtMs(),
|
||||
ScoreStartMS: item.GetScoreStartMs(),
|
||||
ScoreEndMS: item.GetScoreEndMs(),
|
||||
ReviewedByAdminID: item.GetReviewedByAdminId(),
|
||||
GrantedAtMS: item.GetGrantedAtMs(),
|
||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||
}
|
||||
|
||||
102
server/admin/internal/modules/agencyopening/handler_test.go
Normal file
102
server/admin/internal/modules/agencyopening/handler_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package agencyopening
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestApproveApplicationForwardsReviewRequest(t *testing.T) {
|
||||
activity := &fakeAgencyOpeningActivityClient{}
|
||||
router := newAgencyOpeningHandlerTestRouter(New(activity, nil))
|
||||
request := httptest.NewRequest(http.MethodPost, "/admin/activity/agency-opening/applications/application-1/approve", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if activity.lastReview == nil ||
|
||||
activity.lastReview.GetApplicationId() != "application-1" ||
|
||||
activity.lastReview.GetAction() != "approve" ||
|
||||
activity.lastReview.GetOperatorAdminId() != 7 ||
|
||||
activity.lastReview.GetMeta().GetAppCode() != "lalu" ||
|
||||
activity.lastReview.GetMeta().GetCaller() != "admin-server" {
|
||||
t.Fatalf("review request mismatch: %+v", activity.lastReview)
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
ApplicationID string `json:"application_id"`
|
||||
Status string `json:"status"`
|
||||
ApprovedAtMS int64 `json:"approved_at_ms"`
|
||||
ScoreStartMS int64 `json:"score_start_ms"`
|
||||
ScoreEndMS int64 `json:"score_end_ms"`
|
||||
ReviewedByAdminID int64 `json:"reviewed_by_admin_id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if envelope.Code != 0 ||
|
||||
envelope.Data.ApplicationID != "application-1" ||
|
||||
envelope.Data.Status != "approved" ||
|
||||
envelope.Data.ApprovedAtMS != 2000 ||
|
||||
envelope.Data.ScoreStartMS != 2000 ||
|
||||
envelope.Data.ScoreEndMS != 2000+24*60*60*1000 ||
|
||||
envelope.Data.ReviewedByAdminID != 7 {
|
||||
t.Fatalf("approve response mismatch: %+v", envelope)
|
||||
}
|
||||
}
|
||||
|
||||
func newAgencyOpeningHandlerTestRouter(handler *Handler) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
// 这里模拟后台鉴权中间件已经把 app_code、request_id 和操作者写入上下文,测试只关注接口参数透传。
|
||||
c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
|
||||
c.Set(middleware.ContextRequestID, "agency-opening-handler-test")
|
||||
c.Set(middleware.ContextUserID, uint(7))
|
||||
c.Set(middleware.ContextUsername, "tester")
|
||||
c.Next()
|
||||
})
|
||||
router.POST("/admin/activity/agency-opening/applications/:application_id/approve", handler.ApproveApplication)
|
||||
return router
|
||||
}
|
||||
|
||||
type fakeAgencyOpeningActivityClient struct {
|
||||
activityclient.Client
|
||||
|
||||
lastReview *activityv1.ReviewAgencyOpeningApplicationRequest
|
||||
}
|
||||
|
||||
func (f *fakeAgencyOpeningActivityClient) ReviewAgencyOpeningApplication(_ context.Context, req *activityv1.ReviewAgencyOpeningApplicationRequest) (*activityv1.ReviewAgencyOpeningApplicationResponse, error) {
|
||||
f.lastReview = req
|
||||
return &activityv1.ReviewAgencyOpeningApplicationResponse{
|
||||
Application: &activityv1.AgencyOpeningApplication{
|
||||
ApplicationId: req.GetApplicationId(),
|
||||
CycleId: "cycle-1",
|
||||
AgencyId: 8001,
|
||||
AgencyOwnerUserId: 42001,
|
||||
AgencyName: "Window Agency",
|
||||
HostCount: 12,
|
||||
Status: "approved",
|
||||
AppliedAtMs: 1000,
|
||||
ApprovedAtMs: 2000,
|
||||
ScoreStartMs: 2000,
|
||||
ScoreEndMs: 2000 + int64((24 * 60 * 60 * 1000)),
|
||||
ReviewedByAdminId: req.GetOperatorAdminId(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@ -16,4 +16,5 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
protected.PUT("/admin/activity/agency-opening/cycles/:cycle_id", middleware.RequirePermission("agency-opening:update"), h.UpdateCycle)
|
||||
protected.PATCH("/admin/activity/agency-opening/cycles/:cycle_id/status", middleware.RequirePermission("agency-opening:update"), h.SetCycleStatus)
|
||||
protected.GET("/admin/activity/agency-opening/applications", middleware.RequirePermission("agency-opening:view"), h.ListApplications)
|
||||
protected.POST("/admin/activity/agency-opening/applications/:application_id/approve", middleware.RequirePermission("agency-opening:update"), h.ApproveApplication)
|
||||
}
|
||||
|
||||
@ -1352,12 +1352,13 @@ CREATE TABLE IF NOT EXISTS agency_opening_cycles (
|
||||
CREATE TABLE IF NOT EXISTS agency_opening_cycle_rewards (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||
rank_no INT NOT NULL COMMENT '榜单名次',
|
||||
rank_no INT NOT NULL COMMENT '档位序号',
|
||||
threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '命中该档需要的代理房间流水金币',
|
||||
reward_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '奖励金币',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, cycle_id, rank_no)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业榜单奖励';
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业流水奖励档位';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agency_opening_applications (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
@ -1368,14 +1369,19 @@ CREATE TABLE IF NOT EXISTS agency_opening_applications (
|
||||
agency_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '代理名称快照',
|
||||
host_count INT NOT NULL DEFAULT 0 COMMENT '申请时有效主播数快照',
|
||||
agency_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '代理创建时间快照',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/pending/granted/failed',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/approved/pending/granted/failed/expired',
|
||||
score_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '周期内开业流水金币',
|
||||
reward_rank_no INT NOT NULL DEFAULT 0 COMMENT '结算榜单名次',
|
||||
reward_threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '结算命中的流水阈值',
|
||||
reward_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '结算奖励金币',
|
||||
wallet_command_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包幂等命令',
|
||||
wallet_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
applied_at_ms BIGINT NOT NULL COMMENT '申请时间',
|
||||
approved_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '后台同意时间',
|
||||
score_start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分开始时间',
|
||||
score_end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分截止时间',
|
||||
reviewed_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '审核管理员',
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '发奖时间',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
|
||||
@ -40,7 +40,7 @@ CREATE TABLE IF NOT EXISTS agency_opening_applications (
|
||||
agency_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '代理名称快照',
|
||||
host_count INT NOT NULL DEFAULT 0 COMMENT '申请时有效主播数快照',
|
||||
agency_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '代理创建时间快照',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/pending/granted/failed',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/approved/pending/granted/failed/expired',
|
||||
score_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '周期内开业流水金币',
|
||||
reward_rank_no INT NOT NULL DEFAULT 0 COMMENT '结算命中的档位序号',
|
||||
reward_threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '结算命中的流水阈值',
|
||||
@ -49,6 +49,10 @@ CREATE TABLE IF NOT EXISTS agency_opening_applications (
|
||||
wallet_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
applied_at_ms BIGINT NOT NULL COMMENT '申请时间',
|
||||
approved_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '后台同意时间',
|
||||
score_start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分开始时间',
|
||||
score_end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分截止时间',
|
||||
reviewed_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '审核管理员',
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '发奖时间',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
|
||||
@ -9,10 +9,12 @@ const (
|
||||
StatusSettling = "settling"
|
||||
StatusSettled = "settled"
|
||||
|
||||
ApplicationStatusApplied = "applied"
|
||||
ApplicationStatusPending = "pending"
|
||||
ApplicationStatusGranted = "granted"
|
||||
ApplicationStatusFailed = "failed"
|
||||
ApplicationStatusApplied = "applied"
|
||||
ApplicationStatusApproved = "approved"
|
||||
ApplicationStatusPending = "pending"
|
||||
ApplicationStatusGranted = "granted"
|
||||
ApplicationStatusFailed = "failed"
|
||||
ApplicationStatusExpired = "expired"
|
||||
|
||||
EventStatusConsumed = "consumed"
|
||||
EventStatusDuplicate = "duplicate"
|
||||
@ -86,6 +88,10 @@ type Application struct {
|
||||
WalletTransactionID string
|
||||
FailureReason string
|
||||
AppliedAtMS int64
|
||||
ApprovedAtMS int64
|
||||
ScoreStartMS int64
|
||||
ScoreEndMS int64
|
||||
ReviewedByAdminID int64
|
||||
GrantedAtMS int64
|
||||
CreatedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
|
||||
@ -27,6 +27,7 @@ type Repository interface {
|
||||
FindCurrentAgencyOpeningCycle(ctx context.Context, nowMS int64) (domain.Cycle, bool, error)
|
||||
GetAgencyOpeningApplicationByOwner(ctx context.Context, ownerUserID int64) (domain.Application, bool, error)
|
||||
CreateAgencyOpeningApplication(ctx context.Context, cycle domain.Cycle, agency domain.AgencySnapshot, nowMS int64) (domain.Application, bool, error)
|
||||
ApproveAgencyOpeningApplication(ctx context.Context, applicationID string, operatorAdminID int64, nowMS int64) (domain.Application, error)
|
||||
ListAgencyOpeningApplications(ctx context.Context, query domain.ApplicationQuery) ([]domain.Application, int64, error)
|
||||
ConsumeAgencyOpeningGiftEvent(ctx context.Context, event domain.GiftEvent, nowMS int64) (domain.EventResult, error)
|
||||
ClaimDueAgencyOpeningCycles(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int32) ([]domain.Cycle, error)
|
||||
@ -163,6 +164,21 @@ func (s *Service) Apply(ctx context.Context, userID int64, commandID string) (do
|
||||
return s.repository.CreateAgencyOpeningApplication(ctx, qualification.Cycle, qualification.Agency, s.now().UTC().UnixMilli())
|
||||
}
|
||||
|
||||
func (s *Service) ApproveApplication(ctx context.Context, applicationID string, operatorAdminID int64) (domain.Application, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return domain.Application{}, err
|
||||
}
|
||||
applicationID = strings.TrimSpace(applicationID)
|
||||
if applicationID == "" {
|
||||
return domain.Application{}, xerr.New(xerr.InvalidArgument, "application_id is required")
|
||||
}
|
||||
if operatorAdminID <= 0 {
|
||||
return domain.Application{}, xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
|
||||
}
|
||||
// 开业积分必须从后台同意这一刻重新开始计算,不能把用户申请后、审核前的房间礼物补进活动分。
|
||||
return s.repository.ApproveAgencyOpeningApplication(ctx, applicationID, operatorAdminID, s.now().UTC().UnixMilli())
|
||||
}
|
||||
|
||||
func (s *Service) ListApplications(ctx context.Context, query domain.ApplicationQuery) ([]domain.Application, int64, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return nil, 0, err
|
||||
|
||||
@ -141,6 +141,9 @@ func (r *agencyOpeningFakeRepo) GetAgencyOpeningApplicationByOwner(_ context.Con
|
||||
func (r *agencyOpeningFakeRepo) CreateAgencyOpeningApplication(context.Context, domain.Cycle, domain.AgencySnapshot, int64) (domain.Application, bool, error) {
|
||||
return domain.Application{}, false, nil
|
||||
}
|
||||
func (r *agencyOpeningFakeRepo) ApproveAgencyOpeningApplication(context.Context, string, int64, int64) (domain.Application, error) {
|
||||
return domain.Application{}, nil
|
||||
}
|
||||
func (r *agencyOpeningFakeRepo) ListAgencyOpeningApplications(context.Context, domain.ApplicationQuery) ([]domain.Application, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ func (r *Repository) ensureAgencyOpeningTables(ctx context.Context) error {
|
||||
agency_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '代理名称快照',
|
||||
host_count INT NOT NULL DEFAULT 0 COMMENT '申请时有效主播数快照',
|
||||
agency_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '代理创建时间快照',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/pending/granted/failed',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/approved/pending/granted/failed/expired',
|
||||
score_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '历史字段名:周期内开业房间贡献值 HeatValue',
|
||||
reward_rank_no INT NOT NULL DEFAULT 0 COMMENT '结算命中的档位序号',
|
||||
reward_threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '结算命中的流水阈值',
|
||||
@ -53,6 +53,10 @@ func (r *Repository) ensureAgencyOpeningTables(ctx context.Context) error {
|
||||
wallet_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
applied_at_ms BIGINT NOT NULL COMMENT '申请时间',
|
||||
approved_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '后台同意时间',
|
||||
score_start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分开始时间',
|
||||
score_end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分截止时间',
|
||||
reviewed_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '审核管理员',
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '发奖时间',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
@ -105,6 +109,34 @@ func (r *Repository) ensureAgencyOpeningRewardTierColumns(ctx context.Context) e
|
||||
return err
|
||||
}
|
||||
}
|
||||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "approved_at_ms"); err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN approved_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '后台同意时间' AFTER applied_at_ms`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "score_start_ms"); err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN score_start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分开始时间' AFTER approved_at_ms`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "score_end_ms"); err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN score_end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '计分截止时间' AFTER score_start_ms`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if exists, err := r.columnExists(ctx, "agency_opening_applications", "reviewed_by_admin_id"); err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN reviewed_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '审核管理员' AFTER score_end_ms`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -217,8 +217,8 @@ func (r *Repository) CreateAgencyOpeningApplication(ctx context.Context, cycle d
|
||||
INSERT IGNORE INTO agency_opening_applications (
|
||||
app_code, application_id, cycle_id, agency_id, agency_owner_user_id, agency_name, host_count, agency_created_at_ms,
|
||||
status, score_coin_amount, reward_rank_no, reward_threshold_coin_spent, reward_coin_amount, wallet_command_id, wallet_transaction_id, failure_reason,
|
||||
applied_at_ms, granted_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, 0, '', '', '', ?, 0, ?, ?)`,
|
||||
applied_at_ms, approved_at_ms, score_start_ms, score_end_ms, reviewed_by_admin_id, granted_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, 0, '', '', '', ?, 0, 0, 0, 0, 0, ?, ?)`,
|
||||
appcode.FromContext(ctx), applicationID, cycle.CycleID, agency.AgencyID, agency.OwnerUserID, agency.Name, agency.HostCount, agency.AgencyCreatedAtMS,
|
||||
domain.ApplicationStatusApplied, nowMS, nowMS, nowMS)
|
||||
if err != nil {
|
||||
@ -229,6 +229,71 @@ func (r *Repository) CreateAgencyOpeningApplication(ctx context.Context, cycle d
|
||||
return item, affected > 0 && ok, err
|
||||
}
|
||||
|
||||
func (r *Repository) ApproveAgencyOpeningApplication(ctx context.Context, applicationID string, operatorAdminID int64, nowMS int64) (domain.Application, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return domain.Application{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return domain.Application{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var app domain.Application
|
||||
var cycleEndMS int64
|
||||
var cycleStatus string
|
||||
err = tx.QueryRowContext(ctx, agencyOpeningApplicationSelect()+`
|
||||
WHERE app_code = ? AND application_id = ?
|
||||
FOR UPDATE`, appcode.FromContext(ctx), strings.TrimSpace(applicationID)).Scan(
|
||||
&app.AppCode, &app.ApplicationID, &app.CycleID, &app.AgencyID, &app.AgencyOwnerUserID, &app.AgencyName,
|
||||
&app.HostCount, &app.AgencyCreatedAtMS, &app.Status, &app.ScoreCoinAmount, &app.RewardRankNo, &app.RewardThresholdCoin, &app.RewardCoinAmount,
|
||||
&app.WalletCommandID, &app.WalletTransactionID, &app.FailureReason, &app.AppliedAtMS, &app.ApprovedAtMS, &app.ScoreStartMS, &app.ScoreEndMS,
|
||||
&app.ReviewedByAdminID, &app.GrantedAtMS, &app.CreatedAtMS, &app.UpdatedAtMS)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.Application{}, xerr.New(xerr.NotFound, "agency opening application not found")
|
||||
}
|
||||
return domain.Application{}, err
|
||||
}
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
SELECT status, end_ms
|
||||
FROM agency_opening_cycles
|
||||
WHERE app_code = ? AND cycle_id = ? AND activity_code = ?`, appcode.FromContext(ctx), app.CycleID, domain.ActivityCode).Scan(&cycleStatus, &cycleEndMS); err != nil {
|
||||
return domain.Application{}, err
|
||||
}
|
||||
if app.Status == domain.ApplicationStatusApproved {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.Application{}, err
|
||||
}
|
||||
return r.getAgencyOpeningApplication(ctx, app.ApplicationID)
|
||||
}
|
||||
if app.Status != domain.ApplicationStatusApplied {
|
||||
return domain.Application{}, xerr.New(xerr.Conflict, "application is not waiting for approval")
|
||||
}
|
||||
if cycleStatus != domain.StatusActive {
|
||||
return domain.Application{}, xerr.New(xerr.Conflict, "activity is not active")
|
||||
}
|
||||
scoreEndMS := nowMS + int64((24 * time.Hour).Milliseconds())
|
||||
if cycleEndMS > 0 && cycleEndMS < scoreEndMS {
|
||||
scoreEndMS = cycleEndMS
|
||||
}
|
||||
if scoreEndMS <= nowMS {
|
||||
return domain.Application{}, xerr.New(xerr.Conflict, "activity period is already ended")
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE agency_opening_applications
|
||||
SET status = ?, approved_at_ms = ?, score_start_ms = ?, score_end_ms = ?, reviewed_by_admin_id = ?, score_coin_amount = 0,
|
||||
failure_reason = '', updated_at_ms = ?
|
||||
WHERE app_code = ? AND application_id = ? AND status = ?`,
|
||||
domain.ApplicationStatusApproved, nowMS, nowMS, scoreEndMS, operatorAdminID, nowMS,
|
||||
appcode.FromContext(ctx), app.ApplicationID, domain.ApplicationStatusApplied); err != nil {
|
||||
return domain.Application{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.Application{}, err
|
||||
}
|
||||
return r.getAgencyOpeningApplication(ctx, app.ApplicationID)
|
||||
}
|
||||
|
||||
func (r *Repository) ListAgencyOpeningApplications(ctx context.Context, query domain.ApplicationQuery) ([]domain.Application, int64, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
@ -274,17 +339,18 @@ func (r *Repository) ConsumeAgencyOpeningGiftEvent(ctx context.Context, event do
|
||||
err = tx.QueryRowContext(ctx, `
|
||||
SELECT app.app_code, app.application_id, app.cycle_id, app.agency_id, app.agency_owner_user_id, app.agency_name, app.host_count, app.agency_created_at_ms,
|
||||
app.status, app.score_coin_amount, app.reward_rank_no, app.reward_threshold_coin_spent, app.reward_coin_amount, app.wallet_command_id, app.wallet_transaction_id, app.failure_reason,
|
||||
app.applied_at_ms, app.granted_at_ms, app.created_at_ms, app.updated_at_ms
|
||||
app.applied_at_ms, app.approved_at_ms, app.score_start_ms, app.score_end_ms, app.reviewed_by_admin_id, app.granted_at_ms, app.created_at_ms, app.updated_at_ms
|
||||
FROM agency_opening_applications app
|
||||
INNER JOIN agency_opening_cycles cycle ON cycle.app_code = app.app_code AND cycle.cycle_id = app.cycle_id
|
||||
WHERE app.app_code = ? AND app.agency_owner_user_id = ? AND app.status IN (?, ?, ?) AND app.applied_at_ms <= ?
|
||||
WHERE app.app_code = ? AND app.agency_owner_user_id = ? AND app.status = ? AND app.score_start_ms <= ? AND app.score_end_ms > ?
|
||||
AND cycle.activity_code = ? AND cycle.status IN (?, ?) AND cycle.start_ms <= ? AND cycle.end_ms > ?
|
||||
ORDER BY app.applied_at_ms DESC
|
||||
LIMIT 1`, appcode.FromContext(ctx), event.RoomOwnerUserID, domain.ApplicationStatusApplied, domain.ApplicationStatusPending, domain.ApplicationStatusFailed, event.OccurredAtMS,
|
||||
LIMIT 1`, appcode.FromContext(ctx), event.RoomOwnerUserID, domain.ApplicationStatusApproved, event.OccurredAtMS, event.OccurredAtMS,
|
||||
domain.ActivityCode, domain.StatusActive, domain.StatusSettling, event.OccurredAtMS, event.OccurredAtMS).Scan(
|
||||
&app.AppCode, &app.ApplicationID, &app.CycleID, &app.AgencyID, &app.AgencyOwnerUserID, &app.AgencyName,
|
||||
&app.HostCount, &app.AgencyCreatedAtMS, &app.Status, &app.ScoreCoinAmount, &app.RewardRankNo, &app.RewardThresholdCoin, &app.RewardCoinAmount,
|
||||
&app.WalletCommandID, &app.WalletTransactionID, &app.FailureReason, &app.AppliedAtMS, &app.GrantedAtMS, &app.CreatedAtMS, &app.UpdatedAtMS)
|
||||
&app.WalletCommandID, &app.WalletTransactionID, &app.FailureReason, &app.AppliedAtMS, &app.ApprovedAtMS, &app.ScoreStartMS, &app.ScoreEndMS,
|
||||
&app.ReviewedByAdminID, &app.GrantedAtMS, &app.CreatedAtMS, &app.UpdatedAtMS)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusSkipped}, tx.Commit()
|
||||
@ -370,10 +436,18 @@ func (r *Repository) PrepareAgencyOpeningSettlements(ctx context.Context, cycle
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
// 周期结算时仍未后台同意的申请没有有效 24 小时计分窗口,直接过期,不进入发奖队列。
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE agency_opening_applications
|
||||
SET status = ?, failure_reason = 'not approved before cycle ended', updated_at_ms = ?
|
||||
WHERE app_code = ? AND cycle_id = ? AND status = ?`,
|
||||
domain.ApplicationStatusExpired, nowMS, appcode.FromContext(ctx), cycle.CycleID, domain.ApplicationStatusApplied); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := tx.QueryContext(ctx, agencyOpeningApplicationSelect()+`
|
||||
WHERE app_code = ? AND cycle_id = ? AND status IN (?, ?)
|
||||
ORDER BY score_coin_amount DESC, applied_at_ms ASC, agency_id ASC`,
|
||||
appcode.FromContext(ctx), cycle.CycleID, domain.ApplicationStatusApplied, domain.ApplicationStatusFailed)
|
||||
appcode.FromContext(ctx), cycle.CycleID, domain.ApplicationStatusApproved, domain.ApplicationStatusFailed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -405,7 +479,7 @@ func (r *Repository) PrepareAgencyOpeningSettlements(ctx context.Context, cycle
|
||||
WHERE app_code = ? AND application_id = ? AND status IN (?, ?)`,
|
||||
domain.ApplicationStatusGranted, domain.ApplicationStatusPending,
|
||||
reward.RankNo, reward.ThresholdCoinSpent, reward.RewardCoinAmount, walletCommandID, nowMS,
|
||||
appcode.FromContext(ctx), application.ApplicationID, domain.ApplicationStatusApplied, domain.ApplicationStatusFailed); err != nil {
|
||||
appcode.FromContext(ctx), application.ApplicationID, domain.ApplicationStatusApproved, domain.ApplicationStatusFailed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -578,7 +652,7 @@ func agencyOpeningApplicationWhere(ctx context.Context, query domain.Application
|
||||
func agencyOpeningApplicationSelect() string {
|
||||
return `SELECT app_code, application_id, cycle_id, agency_id, agency_owner_user_id, agency_name, host_count, agency_created_at_ms,
|
||||
status, score_coin_amount, reward_rank_no, reward_threshold_coin_spent, reward_coin_amount, wallet_command_id, wallet_transaction_id, failure_reason,
|
||||
applied_at_ms, granted_at_ms, created_at_ms, updated_at_ms FROM agency_opening_applications`
|
||||
applied_at_ms, approved_at_ms, score_start_ms, score_end_ms, reviewed_by_admin_id, granted_at_ms, created_at_ms, updated_at_ms FROM agency_opening_applications`
|
||||
}
|
||||
|
||||
func scanAgencyOpeningCycle(row interface{ Scan(...any) error }) (domain.Cycle, error) {
|
||||
@ -593,7 +667,8 @@ func scanAgencyOpeningApplication(row interface{ Scan(...any) error }) (domain.A
|
||||
var item domain.Application
|
||||
err := row.Scan(&item.AppCode, &item.ApplicationID, &item.CycleID, &item.AgencyID, &item.AgencyOwnerUserID, &item.AgencyName,
|
||||
&item.HostCount, &item.AgencyCreatedAtMS, &item.Status, &item.ScoreCoinAmount, &item.RewardRankNo, &item.RewardThresholdCoin, &item.RewardCoinAmount,
|
||||
&item.WalletCommandID, &item.WalletTransactionID, &item.FailureReason, &item.AppliedAtMS, &item.GrantedAtMS, &item.CreatedAtMS, &item.UpdatedAtMS)
|
||||
&item.WalletCommandID, &item.WalletTransactionID, &item.FailureReason, &item.AppliedAtMS, &item.ApprovedAtMS, &item.ScoreStartMS, &item.ScoreEndMS,
|
||||
&item.ReviewedByAdminID, &item.GrantedAtMS, &item.CreatedAtMS, &item.UpdatedAtMS)
|
||||
return item, err
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"hyapp/internal/testutil/mysqlschema"
|
||||
"hyapp/pkg/appcode"
|
||||
domain "hyapp/services/activity-service/internal/domain/agencyopening"
|
||||
)
|
||||
|
||||
func TestAgencyOpeningScoresOnlyAfterApprovalWithinWindow(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_agency_opening_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
const (
|
||||
cycleID = "cycle-window"
|
||||
applicationID = "application-window"
|
||||
ownerUserID = int64(42001)
|
||||
nowMS = int64(10_000)
|
||||
)
|
||||
_, err = repository.db.ExecContext(ctx, `
|
||||
INSERT INTO agency_opening_cycles (
|
||||
app_code, cycle_id, activity_code, title, status, start_ms, end_ms, min_host_count, max_agency_age_days,
|
||||
created_by_admin_id, updated_by_admin_id, settled_at_ms, locked_by, lock_until_ms, created_at_ms, updated_at_ms
|
||||
) VALUES ('lalu', ?, ?, 'window test', ?, 1, ?, 10, 30, 1, 1, 0, '', 0, ?, ?)`,
|
||||
cycleID, domain.ActivityCode, domain.StatusActive, nowMS+int64((48*60*60*1000)), nowMS, nowMS,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("seed cycle: %v", err)
|
||||
}
|
||||
_, err = repository.db.ExecContext(ctx, `
|
||||
INSERT INTO agency_opening_applications (
|
||||
app_code, application_id, cycle_id, agency_id, agency_owner_user_id, agency_name, host_count, agency_created_at_ms,
|
||||
status, score_coin_amount, reward_rank_no, reward_threshold_coin_spent, reward_coin_amount,
|
||||
wallet_command_id, wallet_transaction_id, failure_reason, applied_at_ms, approved_at_ms,
|
||||
score_start_ms, score_end_ms, reviewed_by_admin_id, granted_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES ('lalu', ?, ?, 8001, ?, 'Window Agency', 12, 1, ?, 0, 0, 0, 0, '', '', '', ?, 0, 0, 0, 0, 0, ?, ?)`,
|
||||
applicationID, cycleID, ownerUserID, domain.ApplicationStatusApplied, nowMS-1000, nowMS, nowMS,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("seed application: %v", err)
|
||||
}
|
||||
|
||||
beforeApproval, err := repository.ConsumeAgencyOpeningGiftEvent(ctx, domain.GiftEvent{
|
||||
EventID: "gift-before-approval",
|
||||
RoomOwnerUserID: ownerUserID,
|
||||
TargetUserID: 99,
|
||||
CoinSpent: 700,
|
||||
OccurredAtMS: nowMS + 100,
|
||||
}, nowMS+100)
|
||||
if err != nil {
|
||||
t.Fatalf("consume before approval: %v", err)
|
||||
}
|
||||
if beforeApproval.Status != domain.EventStatusSkipped {
|
||||
t.Fatalf("event before approval must be skipped, got %s", beforeApproval.Status)
|
||||
}
|
||||
|
||||
approved, err := repository.ApproveAgencyOpeningApplication(ctx, applicationID, 77, nowMS+200)
|
||||
if err != nil {
|
||||
t.Fatalf("approve application: %v", err)
|
||||
}
|
||||
wantScoreEnd := nowMS + 200 + int64((24 * 60 * 60 * 1000))
|
||||
if approved.Status != domain.ApplicationStatusApproved || approved.ScoreStartMS != nowMS+200 || approved.ScoreEndMS != wantScoreEnd || approved.ReviewedByAdminID != 77 {
|
||||
t.Fatalf("unexpected approved application: %+v", approved)
|
||||
}
|
||||
|
||||
insideWindow, err := repository.ConsumeAgencyOpeningGiftEvent(ctx, domain.GiftEvent{
|
||||
EventID: "gift-inside-window",
|
||||
RoomOwnerUserID: ownerUserID,
|
||||
TargetUserID: 100,
|
||||
CoinSpent: 900,
|
||||
OccurredAtMS: nowMS + 300,
|
||||
}, nowMS+300)
|
||||
if err != nil {
|
||||
t.Fatalf("consume inside window: %v", err)
|
||||
}
|
||||
if insideWindow.Status != domain.EventStatusConsumed {
|
||||
t.Fatalf("event inside approved window must be consumed, got %s", insideWindow.Status)
|
||||
}
|
||||
|
||||
afterWindow, err := repository.ConsumeAgencyOpeningGiftEvent(ctx, domain.GiftEvent{
|
||||
EventID: "gift-after-window",
|
||||
RoomOwnerUserID: ownerUserID,
|
||||
TargetUserID: 101,
|
||||
CoinSpent: 1200,
|
||||
OccurredAtMS: wantScoreEnd,
|
||||
}, wantScoreEnd)
|
||||
if err != nil {
|
||||
t.Fatalf("consume after window: %v", err)
|
||||
}
|
||||
if afterWindow.Status != domain.EventStatusSkipped {
|
||||
t.Fatalf("event at score_end must be skipped, got %s", afterWindow.Status)
|
||||
}
|
||||
|
||||
var score int64
|
||||
if err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT score_coin_amount FROM agency_opening_applications
|
||||
WHERE app_code = 'lalu' AND application_id = ?`, applicationID).Scan(&score); err != nil {
|
||||
t.Fatalf("read final score: %v", err)
|
||||
}
|
||||
if score != 900 {
|
||||
t.Fatalf("only the inside-window event should score, got %d", score)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,216 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcpkg "google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
"google.golang.org/protobuf/proto"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
agencydomain "hyapp/services/activity-service/internal/domain/agencyopening"
|
||||
agencyservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||
"hyapp/services/activity-service/internal/testutil/mysqltest"
|
||||
)
|
||||
|
||||
func TestAgencyOpeningGRPCInterfaceFlow(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
now := time.Now().UTC()
|
||||
ownerUserID := int64(42001)
|
||||
agencySvc := agencyservice.New(repository, nil, fakeAgencyOpeningFlowSource{
|
||||
snapshot: agencydomain.AgencySnapshot{
|
||||
AgencyID: 8001,
|
||||
OwnerUserID: ownerUserID,
|
||||
Name: "Window Agency",
|
||||
HostCount: 12,
|
||||
AgencyCreatedAtMS: now.Add(-time.Hour).UnixMilli(),
|
||||
Status: "active",
|
||||
},
|
||||
}, &fakeAgencyOpeningFlowRoom{ownerUserID: ownerUserID})
|
||||
conn := newAgencyOpeningFlowGRPCConn(t, agencySvc, repository)
|
||||
|
||||
adminClient := activityv1.NewAdminAgencyOpeningServiceClient(conn)
|
||||
appClient := activityv1.NewAgencyOpeningServiceClient(conn)
|
||||
roomEventClient := activityv1.NewRoomEventConsumerServiceClient(conn)
|
||||
ctx := context.Background()
|
||||
|
||||
createdCycle, err := adminClient.CreateAgencyOpeningCycle(ctx, &activityv1.UpsertAgencyOpeningCycleRequest{
|
||||
Meta: agencyOpeningFlowMeta("agency-opening-cycle-create"),
|
||||
Cycle: &activityv1.AgencyOpeningCycle{
|
||||
Title: "Agency Opening Interface Flow",
|
||||
Status: "active",
|
||||
StartMs: now.Add(-time.Minute).UnixMilli(),
|
||||
EndMs: now.Add(48 * time.Hour).UnixMilli(),
|
||||
MinHostCount: 10,
|
||||
MaxAgencyAgeDays: 30,
|
||||
Rewards: []*activityv1.AgencyOpeningReward{{
|
||||
RankNo: 1,
|
||||
ThresholdCoinSpent: 500,
|
||||
RewardCoinAmount: 50,
|
||||
}},
|
||||
},
|
||||
OperatorAdminId: 7,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create agency opening cycle failed: %v", err)
|
||||
}
|
||||
if !createdCycle.GetCreated() || createdCycle.GetCycle().GetCycleId() == "" {
|
||||
t.Fatalf("create cycle response mismatch: %+v", createdCycle)
|
||||
}
|
||||
|
||||
beforeApply, err := appClient.GetAgencyOpeningStatus(ctx, &activityv1.GetAgencyOpeningStatusRequest{
|
||||
Meta: agencyOpeningFlowMeta("agency-opening-status-before-apply"),
|
||||
UserId: ownerUserID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get status before apply failed: %v", err)
|
||||
}
|
||||
if beforeApply.GetJoined() || !beforeApply.GetEligible() {
|
||||
t.Fatalf("owner should be eligible before apply: %+v", beforeApply)
|
||||
}
|
||||
|
||||
applied, err := appClient.ApplyAgencyOpening(ctx, &activityv1.ApplyAgencyOpeningRequest{
|
||||
Meta: agencyOpeningFlowMeta("agency-opening-apply"),
|
||||
UserId: ownerUserID,
|
||||
CommandId: "cmd-agency-opening-flow-apply",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("apply agency opening failed: %v", err)
|
||||
}
|
||||
if !applied.GetCreated() || applied.GetApplication().GetStatus() != agencydomain.ApplicationStatusApplied || applied.GetApplication().GetScoreStartMs() != 0 {
|
||||
t.Fatalf("apply response mismatch: %+v", applied)
|
||||
}
|
||||
|
||||
consumeAgencyOpeningFlowGift(t, ctx, roomEventClient, "gift-before-approval", "room-flow-1", ownerUserID, now.UnixMilli(), 700)
|
||||
|
||||
approved, err := adminClient.ReviewAgencyOpeningApplication(ctx, &activityv1.ReviewAgencyOpeningApplicationRequest{
|
||||
Meta: agencyOpeningFlowMeta("agency-opening-approve"),
|
||||
ApplicationId: applied.GetApplication().GetApplicationId(),
|
||||
Action: "approve",
|
||||
OperatorAdminId: 7,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("approve agency opening failed: %v", err)
|
||||
}
|
||||
if approved.GetApplication().GetStatus() != agencydomain.ApplicationStatusApproved ||
|
||||
approved.GetApplication().GetScoreStartMs() != approved.GetApplication().GetApprovedAtMs() ||
|
||||
approved.GetApplication().GetScoreEndMs() <= approved.GetApplication().GetScoreStartMs() {
|
||||
t.Fatalf("approve response should open a scoring window: %+v", approved)
|
||||
}
|
||||
|
||||
insideWindowMS := approved.GetApplication().GetScoreStartMs() + int64(time.Second.Milliseconds())
|
||||
consumeAgencyOpeningFlowGift(t, ctx, roomEventClient, "gift-inside-window", "room-flow-1", ownerUserID, insideWindowMS, 900)
|
||||
|
||||
afterInside, err := appClient.GetAgencyOpeningStatus(ctx, &activityv1.GetAgencyOpeningStatusRequest{
|
||||
Meta: agencyOpeningFlowMeta("agency-opening-status-after-inside"),
|
||||
UserId: ownerUserID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get status after inside-window gift failed: %v", err)
|
||||
}
|
||||
if afterInside.GetApplication().GetScoreCoinAmount() != 900 ||
|
||||
afterInside.GetApplication().GetScoreEndMs() != approved.GetApplication().GetScoreEndMs() {
|
||||
t.Fatalf("inside-window gift should be counted once: %+v", afterInside.GetApplication())
|
||||
}
|
||||
|
||||
consumeAgencyOpeningFlowGift(t, ctx, roomEventClient, "gift-at-window-end", "room-flow-1", ownerUserID, approved.GetApplication().GetScoreEndMs(), 1200)
|
||||
|
||||
afterWindow, err := appClient.GetAgencyOpeningStatus(ctx, &activityv1.GetAgencyOpeningStatusRequest{
|
||||
Meta: agencyOpeningFlowMeta("agency-opening-status-after-window"),
|
||||
UserId: ownerUserID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get status after window-end gift failed: %v", err)
|
||||
}
|
||||
if afterWindow.GetApplication().GetScoreCoinAmount() != 900 {
|
||||
t.Fatalf("window-end gift must not be counted: %+v", afterWindow.GetApplication())
|
||||
}
|
||||
}
|
||||
|
||||
func newAgencyOpeningFlowGRPCConn(t *testing.T, agencySvc *agencyservice.Service, repository *mysqltest.Repository) *grpcpkg.ClientConn {
|
||||
t.Helper()
|
||||
|
||||
listener := bufconn.Listen(1024 * 1024)
|
||||
server := grpcpkg.NewServer()
|
||||
activityv1.RegisterAgencyOpeningServiceServer(server, NewAgencyOpeningServer(agencySvc))
|
||||
activityv1.RegisterAdminAgencyOpeningServiceServer(server, NewAdminAgencyOpeningServer(agencySvc))
|
||||
broadcastSvc := broadcastservice.New(broadcastservice.Config{SuperGiftMinValue: 1_000_000_000}, repository, nil, nil)
|
||||
activityv1.RegisterRoomEventConsumerServiceServer(server, NewBroadcastServer(broadcastSvc, nil, nil, nil, nil, agencySvc))
|
||||
go func() {
|
||||
_ = server.Serve(listener)
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
server.Stop()
|
||||
_ = listener.Close()
|
||||
})
|
||||
|
||||
conn, err := grpcpkg.NewClient("passthrough:///agency-opening-flow-bufconn",
|
||||
grpcpkg.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||
return listener.Dial()
|
||||
}),
|
||||
grpcpkg.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("dial agency opening flow grpc failed: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
return conn
|
||||
}
|
||||
|
||||
func consumeAgencyOpeningFlowGift(t *testing.T, ctx context.Context, client activityv1.RoomEventConsumerServiceClient, eventID string, roomID string, ownerUserID int64, occurredAtMS int64, giftValue int64) {
|
||||
t.Helper()
|
||||
|
||||
body, err := proto.Marshal(&roomeventsv1.RoomGiftSent{
|
||||
SenderUserId: 10001,
|
||||
TargetUserId: ownerUserID,
|
||||
GiftValue: giftValue,
|
||||
CoinSpent: giftValue,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal room gift event failed: %v", err)
|
||||
}
|
||||
if _, err := client.ConsumeRoomEvent(ctx, &activityv1.ConsumeRoomEventRequest{
|
||||
Meta: agencyOpeningFlowMeta("consume-" + eventID),
|
||||
Envelope: &roomeventsv1.EventEnvelope{
|
||||
EventId: eventID,
|
||||
RoomId: roomID,
|
||||
EventType: "RoomGiftSent",
|
||||
OccurredAtMs: occurredAtMS,
|
||||
AppCode: "lalu",
|
||||
Body: body,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("consume room event %s failed: %v", eventID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func agencyOpeningFlowMeta(requestID string) *activityv1.RequestMeta {
|
||||
return &activityv1.RequestMeta{
|
||||
RequestId: requestID,
|
||||
Caller: "agency-opening-interface-flow-test",
|
||||
AppCode: "lalu",
|
||||
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAgencyOpeningFlowSource struct {
|
||||
snapshot agencydomain.AgencySnapshot
|
||||
}
|
||||
|
||||
func (f fakeAgencyOpeningFlowSource) ResolveOwnerAgency(context.Context, int64) (agencydomain.AgencySnapshot, bool, error) {
|
||||
return f.snapshot, true, nil
|
||||
}
|
||||
|
||||
type fakeAgencyOpeningFlowRoom struct {
|
||||
ownerUserID int64
|
||||
}
|
||||
|
||||
func (r *fakeAgencyOpeningFlowRoom) AdminGetRoom(context.Context, *roomv1.AdminGetRoomRequest, ...grpcpkg.CallOption) (*roomv1.AdminGetRoomResponse, error) {
|
||||
return &roomv1.AdminGetRoomResponse{Room: &roomv1.AdminRoomListItem{OwnerUserId: r.ownerUserID}}, nil
|
||||
}
|
||||
@ -122,6 +122,18 @@ func (s *AdminAgencyOpeningServer) ListAgencyOpeningApplications(ctx context.Con
|
||||
return agencyOpeningApplicationsResponse(items, total), nil
|
||||
}
|
||||
|
||||
func (s *AdminAgencyOpeningServer) ReviewAgencyOpeningApplication(ctx context.Context, req *activityv1.ReviewAgencyOpeningApplicationRequest) (*activityv1.ReviewAgencyOpeningApplicationResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
if req.GetAction() != "approve" {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "review action is invalid"))
|
||||
}
|
||||
item, err := s.svc.ApproveApplication(ctx, req.GetApplicationId(), req.GetOperatorAdminId())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.ReviewAgencyOpeningApplicationResponse{Application: agencyOpeningApplicationToProto(item)}, nil
|
||||
}
|
||||
|
||||
func agencyOpeningCycleFromProto(item *activityv1.AgencyOpeningCycle) domain.Cycle {
|
||||
if item == nil {
|
||||
return domain.Cycle{}
|
||||
@ -200,6 +212,10 @@ func agencyOpeningApplicationToProto(item domain.Application) *activityv1.Agency
|
||||
WalletTransactionId: item.WalletTransactionID,
|
||||
FailureReason: item.FailureReason,
|
||||
AppliedAtMs: item.AppliedAtMS,
|
||||
ApprovedAtMs: item.ApprovedAtMS,
|
||||
ScoreStartMs: item.ScoreStartMS,
|
||||
ScoreEndMs: item.ScoreEndMS,
|
||||
ReviewedByAdminId: item.ReviewedByAdminID,
|
||||
GrantedAtMs: item.GrantedAtMS,
|
||||
UpdatedAtMs: item.UpdatedAtMS,
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ type agencyOpeningRecordData struct {
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
OpeningRevenue string `json:"opening_revenue,omitempty"`
|
||||
Reward int64 `json:"reward"`
|
||||
ScoreEndMS int64 `json:"score_end_ms,omitempty"`
|
||||
}
|
||||
|
||||
type agencyOpeningApplicationData struct {
|
||||
@ -84,6 +85,10 @@ type agencyOpeningApplicationData struct {
|
||||
WalletTransactionID string `json:"wallet_transaction_id,omitempty"`
|
||||
FailureReason string `json:"failure_reason,omitempty"`
|
||||
AppliedAtMS int64 `json:"applied_at_ms"`
|
||||
ApprovedAtMS int64 `json:"approved_at_ms,omitempty"`
|
||||
ScoreStartMS int64 `json:"score_start_ms,omitempty"`
|
||||
ScoreEndMS int64 `json:"score_end_ms,omitempty"`
|
||||
ReviewedByAdminID int64 `json:"reviewed_by_admin_id,omitempty"`
|
||||
GrantedAtMS int64 `json:"granted_at_ms,omitempty"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms,omitempty"`
|
||||
}
|
||||
@ -221,11 +226,8 @@ func agencyOpeningQualificationFromProto(status *activityv1.GetAgencyOpeningStat
|
||||
agencyName = application.GetAgencyName()
|
||||
}
|
||||
}
|
||||
openingStatus := "Not Joined"
|
||||
if status.GetJoined() {
|
||||
openingStatus = "Joined"
|
||||
}
|
||||
if !status.GetEnabled() {
|
||||
openingStatus := agencyOpeningStatusLabel(application)
|
||||
if !status.GetEnabled() && !status.GetJoined() {
|
||||
openingStatus = "Disabled"
|
||||
}
|
||||
return agencyOpeningQualificationData{
|
||||
@ -266,6 +268,7 @@ func agencyOpeningRecordFromProto(application *activityv1.AgencyOpeningApplicati
|
||||
CreatedAt: agencyOpeningDate(application.GetAgencyCreatedAtMs()),
|
||||
OpeningRevenue: fmt.Sprintf("%d", application.GetScoreCoinAmount()),
|
||||
Reward: application.GetRewardCoinAmount(),
|
||||
ScoreEndMS: application.GetScoreEndMs(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,11 +301,37 @@ func agencyOpeningApplicationFromProto(item *activityv1.AgencyOpeningApplication
|
||||
WalletTransactionID: item.GetWalletTransactionId(),
|
||||
FailureReason: item.GetFailureReason(),
|
||||
AppliedAtMS: item.GetAppliedAtMs(),
|
||||
ApprovedAtMS: item.GetApprovedAtMs(),
|
||||
ScoreStartMS: item.GetScoreStartMs(),
|
||||
ScoreEndMS: item.GetScoreEndMs(),
|
||||
ReviewedByAdminID: item.GetReviewedByAdminId(),
|
||||
GrantedAtMS: item.GetGrantedAtMs(),
|
||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func agencyOpeningStatusLabel(application *activityv1.AgencyOpeningApplication) string {
|
||||
if application == nil {
|
||||
return "Not Joined"
|
||||
}
|
||||
switch application.GetStatus() {
|
||||
case "applied":
|
||||
return "Pending Review"
|
||||
case "approved":
|
||||
return "Approved"
|
||||
case "pending":
|
||||
return "Pending Reward"
|
||||
case "granted":
|
||||
return "Rewarded"
|
||||
case "failed":
|
||||
return "Reward Failed"
|
||||
case "expired":
|
||||
return "Expired"
|
||||
default:
|
||||
return "Joined"
|
||||
}
|
||||
}
|
||||
|
||||
func formatAgencyOpeningCoinLabel(value int64) string {
|
||||
if value <= 0 {
|
||||
return "0"
|
||||
@ -322,7 +351,8 @@ func agencyOpeningRules(cycle *activityv1.AgencyOpeningCycle) []string {
|
||||
}
|
||||
return []string{
|
||||
fmt.Sprintf("Agency must have more than %d hosts before applying.", cycle.GetMinHostCount()),
|
||||
"Opening revenue comes from the applicant user's room gift COIN revenue during the configured period.",
|
||||
"Opening revenue starts from admin approval and only counts the applicant user's room gift COIN revenue within the next 24 hours.",
|
||||
"If the 24-hour scoring window passes the activity end time, the activity end time is used as the cutoff.",
|
||||
"Reward tier is matched by opening revenue after the period ends; it is not based on Top ranking.",
|
||||
"Rewards are granted automatically after the period ends.",
|
||||
}
|
||||
|
||||
@ -0,0 +1,215 @@
|
||||
package activityapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
)
|
||||
|
||||
func TestGetAgencyOpeningStatusReturnsReviewWindowFields(t *testing.T) {
|
||||
client := &fakeAgencyOpeningClient{statusResp: agencyOpeningApprovedStatusResponse()}
|
||||
handler := New(Config{AgencyOpening: client})
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/activities/agency-opening-event", nil)
|
||||
request = request.WithContext(auth.WithUserID(appcode.WithContext(request.Context(), "lalu"), 42001))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.getAgencyOpeningStatus(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if client.lastStatus == nil || client.lastStatus.GetUserId() != 42001 || client.lastStatus.GetMeta().GetAppCode() != "lalu" {
|
||||
t.Fatalf("status request mismatch: %+v", client.lastStatus)
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
Code string `json:"code"`
|
||||
Data struct {
|
||||
Joined bool `json:"joined"`
|
||||
Qualification struct {
|
||||
OpeningStatus string `json:"opening_status"`
|
||||
} `json:"qualification"`
|
||||
Application struct {
|
||||
Status string `json:"status"`
|
||||
ApprovedAtMS int64 `json:"approved_at_ms"`
|
||||
ScoreStartMS int64 `json:"score_start_ms"`
|
||||
ScoreEndMS int64 `json:"score_end_ms"`
|
||||
ReviewedByAdminID int64 `json:"reviewed_by_admin_id"`
|
||||
} `json:"application"`
|
||||
Rules []string `json:"rules"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if envelope.Code != "OK" || !envelope.Data.Joined || envelope.Data.Qualification.OpeningStatus != "Approved" {
|
||||
t.Fatalf("status envelope mismatch: %+v", envelope)
|
||||
}
|
||||
if envelope.Data.Application.Status != "approved" ||
|
||||
envelope.Data.Application.ApprovedAtMS != 2000 ||
|
||||
envelope.Data.Application.ScoreStartMS != 2000 ||
|
||||
envelope.Data.Application.ScoreEndMS != 2000+24*60*60*1000 ||
|
||||
envelope.Data.Application.ReviewedByAdminID != 7 {
|
||||
t.Fatalf("application fields mismatch: %+v", envelope.Data.Application)
|
||||
}
|
||||
if len(envelope.Data.Rules) < 2 || !strings.Contains(envelope.Data.Rules[1], "24 hours") {
|
||||
t.Fatalf("rules should explain 24-hour scoring window: %+v", envelope.Data.Rules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAgencyOpeningReturnsPendingReviewStatus(t *testing.T) {
|
||||
client := &fakeAgencyOpeningClient{
|
||||
applyResp: agencyOpeningApplyResponse(),
|
||||
statusResp: agencyOpeningAppliedStatusResponse(),
|
||||
}
|
||||
handler := New(Config{AgencyOpening: client})
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/activities/agency-opening-event/apply", strings.NewReader(`{"command_id":"cmd-apply-1"}`))
|
||||
request = request.WithContext(auth.WithUserID(appcode.WithContext(request.Context(), "lalu"), 42001))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.applyAgencyOpening(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if client.lastApply == nil || client.lastApply.GetUserId() != 42001 || client.lastApply.GetCommandId() != "cmd-apply-1" {
|
||||
t.Fatalf("apply request mismatch: %+v", client.lastApply)
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
Code string `json:"code"`
|
||||
Data struct {
|
||||
Joined bool `json:"joined"`
|
||||
Qualification struct {
|
||||
OpeningStatus string `json:"opening_status"`
|
||||
} `json:"qualification"`
|
||||
Application struct {
|
||||
Status string `json:"status"`
|
||||
AppliedAtMS int64 `json:"applied_at_ms"`
|
||||
ScoreStartMS int64 `json:"score_start_ms"`
|
||||
} `json:"application"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if envelope.Code != "OK" || !envelope.Data.Joined || envelope.Data.Qualification.OpeningStatus != "Pending Review" {
|
||||
t.Fatalf("apply response should show pending review: %+v", envelope)
|
||||
}
|
||||
if envelope.Data.Application.Status != "applied" || envelope.Data.Application.AppliedAtMS != 1000 || envelope.Data.Application.ScoreStartMS != 0 {
|
||||
t.Fatalf("apply application fields mismatch: %+v", envelope.Data.Application)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAgencyOpeningClient struct {
|
||||
statusResp *activityv1.GetAgencyOpeningStatusResponse
|
||||
applyResp *activityv1.ApplyAgencyOpeningResponse
|
||||
|
||||
lastStatus *activityv1.GetAgencyOpeningStatusRequest
|
||||
lastApply *activityv1.ApplyAgencyOpeningRequest
|
||||
}
|
||||
|
||||
func (f *fakeAgencyOpeningClient) GetAgencyOpeningStatus(_ context.Context, req *activityv1.GetAgencyOpeningStatusRequest) (*activityv1.GetAgencyOpeningStatusResponse, error) {
|
||||
f.lastStatus = req
|
||||
if f.statusResp != nil {
|
||||
return f.statusResp, nil
|
||||
}
|
||||
return &activityv1.GetAgencyOpeningStatusResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeAgencyOpeningClient) ApplyAgencyOpening(_ context.Context, req *activityv1.ApplyAgencyOpeningRequest) (*activityv1.ApplyAgencyOpeningResponse, error) {
|
||||
f.lastApply = req
|
||||
if f.applyResp != nil {
|
||||
return f.applyResp, nil
|
||||
}
|
||||
return &activityv1.ApplyAgencyOpeningResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeAgencyOpeningClient) ListAgencyOpeningLeaderboard(context.Context, *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, error) {
|
||||
return &activityv1.ListAgencyOpeningApplicationsResponse{}, nil
|
||||
}
|
||||
|
||||
func agencyOpeningApplyResponse() *activityv1.ApplyAgencyOpeningResponse {
|
||||
return &activityv1.ApplyAgencyOpeningResponse{
|
||||
Created: true,
|
||||
Application: &activityv1.AgencyOpeningApplication{
|
||||
ApplicationId: "application-1",
|
||||
CycleId: "cycle-1",
|
||||
AgencyId: 8001,
|
||||
AgencyOwnerUserId: 42001,
|
||||
AgencyName: "Window Agency",
|
||||
HostCount: 12,
|
||||
Status: "applied",
|
||||
AppliedAtMs: 1000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func agencyOpeningAppliedStatusResponse() *activityv1.GetAgencyOpeningStatusResponse {
|
||||
resp := agencyOpeningBaseStatusResponse()
|
||||
resp.Joined = true
|
||||
resp.Eligible = false
|
||||
resp.Application = agencyOpeningApplyResponse().GetApplication()
|
||||
return resp
|
||||
}
|
||||
|
||||
func agencyOpeningApprovedStatusResponse() *activityv1.GetAgencyOpeningStatusResponse {
|
||||
resp := agencyOpeningBaseStatusResponse()
|
||||
resp.Joined = true
|
||||
resp.Eligible = false
|
||||
resp.Application = &activityv1.AgencyOpeningApplication{
|
||||
ApplicationId: "application-1",
|
||||
CycleId: "cycle-1",
|
||||
AgencyId: 8001,
|
||||
AgencyOwnerUserId: 42001,
|
||||
AgencyName: "Window Agency",
|
||||
HostCount: 12,
|
||||
Status: "approved",
|
||||
ScoreCoinAmount: 900,
|
||||
RewardThresholdCoinSpent: 500,
|
||||
RewardCoinAmount: 50,
|
||||
AppliedAtMs: 1000,
|
||||
ApprovedAtMs: 2000,
|
||||
ScoreStartMs: 2000,
|
||||
ScoreEndMs: 2000 + int64((24 * 60 * 60 * 1000)),
|
||||
ReviewedByAdminId: 7,
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func agencyOpeningBaseStatusResponse() *activityv1.GetAgencyOpeningStatusResponse {
|
||||
return &activityv1.GetAgencyOpeningStatusResponse{
|
||||
ServerTimeMs: 3000,
|
||||
Enabled: true,
|
||||
Eligible: true,
|
||||
Joined: false,
|
||||
Cycle: &activityv1.AgencyOpeningCycle{
|
||||
CycleId: "cycle-1",
|
||||
Title: "Agency Opening",
|
||||
Status: "active",
|
||||
StartMs: 1,
|
||||
EndMs: 2000 + int64((48 * 60 * 60 * 1000)),
|
||||
MinHostCount: 10,
|
||||
Rewards: []*activityv1.AgencyOpeningReward{{
|
||||
RankNo: 1,
|
||||
ThresholdCoinSpent: 500,
|
||||
RewardCoinAmount: 50,
|
||||
}},
|
||||
},
|
||||
Agency: &activityv1.AgencyOpeningAgencySnapshot{
|
||||
AgencyId: 8001,
|
||||
OwnerUserId: 42001,
|
||||
Name: "Window Agency",
|
||||
HostCount: 12,
|
||||
AgencyCreatedAtMs: 1,
|
||||
Status: "active",
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user