package store import ( "context" "database/sql" "encoding/json" "errors" "time" "github.com/jmoiron/sqlx" ) const ( orderStatusMatching = 1 orderStatusPending = 2 orderStatusProcessing = 3 orderStatusCompleted = 4 orderStatusExpired = 5 orderStatusFrozen = 6 orderStatusCancelled = 7 orderStatusFailed = 8 ) type TaskHall struct { ID int64 `db:"id"` Code string `db:"hall_code"` Name string `db:"name"` IconURL string `db:"icon_url"` Summary string `db:"summary"` CommissionRate int `db:"commission_rate"` RateUnit string `db:"rate_unit"` ClientRoute string `db:"client_route"` } type TaskEligibility struct { MemberLevelID int64 `db:"member_level_id"` MemberStatus int `db:"member_status"` HallEligible bool `db:"hall_eligible"` AvailableAmount int64 `db:"available_amount"` WalletStatus int `db:"wallet_status"` DailyLimit int `db:"daily_limit"` TodayCount int `db:"today_count"` PendingOrderID int64 `db:"pending_order_id"` } type ClaimTaskInput struct { TenantID int64 MemberID int64 HallID int64 MinimumAmount int64 DeadlineAt time.Time OrderNo string } type TaskOrder struct { ID int64 `db:"id"` OrderNo string `db:"order_no"` TaskID int64 `db:"task_id"` ProductID int64 `db:"product_id"` ProductName string `db:"product_name"` CoverURL string `db:"cover_url"` Quantity int `db:"quantity"` UnitPriceAmount int64 `db:"unit_price_amount"` TotalAmount int64 `db:"total_amount"` CommissionAmount int64 `db:"commission_amount"` Currency string `db:"currency"` MemberLevelID int64 `db:"member_level_id"` RuleSnapshot json.RawMessage `db:"rule_snapshot"` Message string `db:"message"` OrderStatus int `db:"order_status"` FreezeStatus int `db:"freeze_status"` DeadlineAt string `db:"deadline_at"` MatchedAt string `db:"matched_at"` CompletedAt string `db:"completed_at"` CreatedAt string `db:"created_at"` UpdatedAt string `db:"updated_at"` TaskTitle string `db:"task_title"` TaskDescription string `db:"task_description"` TaskDetailHTML string `db:"task_detail_html"` SubmissionNo string `db:"submission_no"` SubmissionStatus int `db:"submission_status"` EvidenceText string `db:"evidence_text"` EvidenceIDs json.RawMessage `db:"evidence_attachment_ids"` ReviewReasonCode string `db:"review_reason_code"` ReviewReason string `db:"review_reason"` AllowResubmit int `db:"allow_resubmit"` SubmittedAt string `db:"submitted_at"` ReviewedAt string `db:"reviewed_at"` SettledAt string `db:"settled_at"` } type TaskOrderPage struct { Items []TaskOrder Total int64 } type TaskStats struct { AvailableAmount int64 `db:"available_amount"` FrozenAmount int64 `db:"frozen_amount"` TodayCommissionAmount int64 `db:"today_commission_amount"` YesterdayCommissionAmount int64 `db:"yesterday_commission_amount"` TodayClaimCount int `db:"today_claim_count"` TodayCompletedCount int `db:"today_completed_count"` } type TaskSubmissionInput struct { TenantID int64 MemberID int64 OrderID int64 SubmissionNo string EvidenceText string AttachmentIDs []int64 } type TaskSubmissionResult struct { SubmissionNo string `db:"submission_no"` OrderStatus int SubmittedAt time.Time } func (s *Store) TaskHalls(ctx context.Context, tenantID, memberID int64) ([]TaskHall, error) { rows := make([]TaskHall, 0) err := s.db.SelectContext(ctx, &rows, `SELECT h.id,h.hall_code,h.name,h.icon_url,h.summary, h.commission_rate,h.rate_unit,h.client_route FROM hg_task_hall h JOIN hg_app_member m ON m.tenant_id=h.tenant_id AND m.id=? AND m.member_status=1 AND m.deleted_at IS NULL JOIN hg_task_hall_level hl ON hl.tenant_id=h.tenant_id AND hl.hall_id=h.id AND hl.level_id=m.level_id WHERE h.tenant_id=? AND h.publish_status=3 AND h.deleted_at IS NULL ORDER BY h.sort,h.id`, memberID, tenantID) return rows, err } func (s *Store) TaskEligibility(ctx context.Context, tenantID, memberID, hallID int64) (*TaskEligibility, error) { var row TaskEligibility err := s.db.GetContext(ctx, &row, `SELECT m.level_id AS member_level_id,m.member_status, EXISTS(SELECT 1 FROM hg_task_hall h JOIN hg_task_hall_level hl ON hl.tenant_id=h.tenant_id AND hl.hall_id=h.id AND hl.level_id=m.level_id WHERE h.tenant_id=m.tenant_id AND h.id=? AND h.publish_status=3 AND h.deleted_at IS NULL) AS hall_eligible, COALESCE(w.available_amount,0) AS available_amount,COALESCE(w.account_status,0) AS wallet_status, COALESCE(lv.daily_order_limit,0) AS daily_limit, (SELECT COUNT(*) FROM hg_trade_order o WHERE o.tenant_id=m.tenant_id AND o.member_id=m.id AND o.order_type=2 AND o.created_at>=UTC_DATE() AND o.created_at 0 { return nil, ErrPendingOrderExists } var dailyLimit int err = tx.GetContext(ctx, &dailyLimit, `SELECT COALESCE(v.daily_order_limit,0) FROM hg_app_member_level l LEFT JOIN hg_app_member_level_version v ON v.tenant_id=l.tenant_id AND v.id=l.current_version_id AND v.publish_status=3 WHERE l.tenant_id=? AND l.id=? AND l.status=1 AND l.deleted_at IS NULL LIMIT 1`, input.TenantID, member.LevelID) if errors.Is(err, sql.ErrNoRows) { return nil, ErrLevelNotEligible } if err != nil { return nil, err } if dailyLimit > 0 { var todayCount int if err := tx.GetContext(ctx, &todayCount, `SELECT COUNT(*) FROM hg_trade_order WHERE tenant_id=? AND member_id=? AND order_type=2 AND created_at>=UTC_DATE() AND created_at= dailyLimit { return nil, ErrDailyLimitReached } } var task struct { ID int64 `db:"id"` Title string `db:"title"` CoverURL string `db:"cover_url"` CommissionAmount int64 `db:"commission_amount"` CommissionRate int `db:"commission_rate"` PerMemberLimit int `db:"per_member_limit"` } err = tx.GetContext(ctx, &task, `SELECT t.id,t.title,t.cover_url,t.commission_amount,t.commission_rate,t.per_member_limit FROM hg_task t JOIN hg_task_level tl ON tl.tenant_id=t.tenant_id AND tl.task_id=t.id AND tl.level_id=? WHERE t.tenant_id=? AND t.hall_id=? AND t.publish_status=3 AND t.deleted_at IS NULL AND (t.start_at IS NULL OR t.start_at<=UTC_TIMESTAMP(3)) AND (t.end_at IS NULL OR t.end_at>UTC_TIMESTAMP(3)) AND (t.daily_limit=0 OR (SELECT COUNT(*) FROM hg_trade_order od WHERE od.tenant_id=t.tenant_id AND od.task_id=t.id AND od.order_type=2 AND od.created_at>=UTC_DATE() AND od.created_at=UTC_DATE() AND om.created_at 0 { where += ` AND o.order_status=?` args = append(args, status) } var total int64 if err := s.db.GetContext(ctx, &total, `SELECT COUNT(*) FROM hg_trade_order o`+where, args...); err != nil { return nil, err } args = append(args, limit, offset) rows := make([]TaskOrder, 0) err := s.db.SelectContext(ctx, &rows, orderSelectSQL()+where+` ORDER BY o.created_at DESC,o.id DESC LIMIT ? OFFSET ?`, args...) return &TaskOrderPage{Items: rows, Total: total}, err } func (s *Store) TaskOrder(ctx context.Context, tenantID, memberID, orderID int64) (*TaskOrder, error) { var row TaskOrder err := s.db.GetContext(ctx, &row, orderSelectSQL()+` WHERE o.tenant_id=? AND o.member_id=? AND o.order_type=2 AND o.id=? LIMIT 1`, tenantID, memberID, orderID) if errors.Is(err, sql.ErrNoRows) { return nil, ErrOrderNotFound } return &row, err } func orderSelectSQL() string { return `SELECT o.id,o.order_no,o.task_id,o.product_id,COALESCE(NULLIF(o.product_name,''),t.title,'') AS product_name, COALESCE(NULLIF(p.cover_url,''),t.cover_url,'') AS cover_url,o.quantity,o.unit_price_amount, o.total_amount,o.commission_amount,o.currency,o.member_level_id, COALESCE(o.rule_snapshot,JSON_OBJECT()) AS rule_snapshot,o.message,o.order_status,o.freeze_status, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(o.rule_snapshot,'$.deadline_at')),'') AS deadline_at, COALESCE(DATE_FORMAT(o.matched_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS matched_at, COALESCE(DATE_FORMAT(o.completed_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS completed_at, DATE_FORMAT(o.created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at, DATE_FORMAT(o.updated_at,'%Y-%m-%dT%H:%i:%sZ') AS updated_at, COALESCE(t.title,'') AS task_title,COALESCE(t.description,'') AS task_description, COALESCE(t.detail_html,'') AS task_detail_html,COALESCE(s.submission_no,'') AS submission_no, COALESCE(s.submission_status,0) AS submission_status,COALESCE(s.evidence_text,'') AS evidence_text, COALESCE(s.evidence_attachment_ids,JSON_ARRAY()) AS evidence_attachment_ids, COALESCE(s.review_reason_code,'') AS review_reason_code,COALESCE(s.review_reason,'') AS review_reason, COALESCE(s.allow_resubmit,2) AS allow_resubmit, COALESCE(DATE_FORMAT(s.created_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS submitted_at, COALESCE(DATE_FORMAT(s.reviewed_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS reviewed_at, COALESCE(DATE_FORMAT(s.settled_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS settled_at FROM hg_trade_order o LEFT JOIN hg_task t ON t.tenant_id=o.tenant_id AND t.id=o.task_id LEFT JOIN hg_product p ON p.tenant_id=o.tenant_id AND p.id=o.product_id LEFT JOIN hg_task_submission s ON s.tenant_id=o.tenant_id AND s.trade_order_id=o.id` } func (s *Store) TaskStats(ctx context.Context, tenantID, memberID int64) (*TaskStats, error) { var row TaskStats err := s.db.GetContext(ctx, &row, `SELECT COALESCE(w.available_amount,0) AS available_amount,COALESCE(w.frozen_amount,0) AS frozen_amount, (SELECT COALESCE(SUM(ts.commission_amount),0) FROM hg_task_submission ts WHERE ts.tenant_id=? AND ts.member_id=? AND ts.submission_status=4 AND ts.settled_at>=UTC_DATE() AND ts.settled_at=UTC_DATE()-INTERVAL 1 DAY AND ys.settled_at=UTC_DATE() AND tc.created_at=UTC_DATE() AND cc.completed_at