Compare commits
No commits in common. "main" and "codex/prize-value-snapshot" have entirely different histories.
main
...
codex/priz
@ -1,38 +0,0 @@
|
||||
# .agents Directory
|
||||
|
||||
This directory contains agent configuration and skills for OpenAI Codex CLI.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
.agents/
|
||||
config.toml # Main configuration file
|
||||
skills/ # Skill definitions
|
||||
skill-name/
|
||||
SKILL.md # Skill instructions
|
||||
scripts/ # Optional scripts
|
||||
docs/ # Optional documentation
|
||||
README.md # This file
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The `config.toml` file controls:
|
||||
- Model selection
|
||||
- Approval policies
|
||||
- Sandbox modes
|
||||
- MCP server connections
|
||||
- Skills configuration
|
||||
|
||||
## Skills
|
||||
|
||||
Skills are invoked using `$skill-name` syntax. Each skill has:
|
||||
- YAML frontmatter with metadata
|
||||
- Trigger and skip conditions
|
||||
- Commands and examples
|
||||
|
||||
## Documentation
|
||||
|
||||
- Main instructions: `AGENTS.md` (project root)
|
||||
- Local overrides: `.codex/AGENTS.override.md` (gitignored)
|
||||
- Claude Flow: https://github.com/ruvnet/claude-flow
|
||||
@ -1,298 +0,0 @@
|
||||
# =============================================================================
|
||||
# Claude Flow V3 - Codex Configuration
|
||||
# =============================================================================
|
||||
# Generated by: @claude-flow/codex
|
||||
# Documentation: https://github.com/ruvnet/claude-flow
|
||||
#
|
||||
# This file configures the Codex CLI for Claude Flow integration.
|
||||
# Place in .agents/config.toml (project) or .codex/config.toml (user).
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Core Settings
|
||||
# =============================================================================
|
||||
|
||||
# Model selection - the AI model to use for code generation
|
||||
# Options: gpt-5.3-codex, gpt-4o, claude-sonnet, claude-opus
|
||||
model = "gpt-5.3-codex"
|
||||
|
||||
# Approval policy determines when human approval is required
|
||||
# - untrusted: Always require approval
|
||||
# - on-failure: Require approval only after failures
|
||||
# - on-request: Require approval for significant changes
|
||||
# - never: Auto-approve all actions (use with caution)
|
||||
approval_policy = "on-request"
|
||||
|
||||
# Sandbox mode controls file system access
|
||||
# - read-only: Can only read files, no modifications
|
||||
# - workspace-write: Can write within workspace directory
|
||||
# - danger-full-access: Full file system access (dangerous)
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
# Web search enables internet access for research
|
||||
# - disabled: No web access
|
||||
# - cached: Use cached results when available
|
||||
# - live: Always fetch fresh results
|
||||
web_search = "cached"
|
||||
|
||||
# =============================================================================
|
||||
# Project Documentation
|
||||
# =============================================================================
|
||||
|
||||
# Maximum bytes to read from AGENTS.md files
|
||||
project_doc_max_bytes = 65536
|
||||
|
||||
# Fallback filenames if AGENTS.md not found
|
||||
project_doc_fallback_filenames = [
|
||||
"AGENTS.md",
|
||||
"TEAM_GUIDE.md",
|
||||
".agents.md"
|
||||
]
|
||||
|
||||
# =============================================================================
|
||||
# Features
|
||||
# =============================================================================
|
||||
|
||||
[features]
|
||||
# Enable child AGENTS.md guidance
|
||||
child_agents_md = true
|
||||
|
||||
# Cache shell environment for faster repeated commands
|
||||
shell_snapshot = true
|
||||
|
||||
# Smart approvals based on request context
|
||||
request_rule = true
|
||||
|
||||
# Enable remote compaction for large histories
|
||||
remote_compaction = true
|
||||
|
||||
# =============================================================================
|
||||
# MCP Servers
|
||||
# =============================================================================
|
||||
|
||||
[mcp_servers.claude-flow]
|
||||
command = "npx"
|
||||
args = ["-y", "@claude-flow/cli@latest"]
|
||||
enabled = true
|
||||
tool_timeout_sec = 120
|
||||
|
||||
# =============================================================================
|
||||
# Skills Configuration
|
||||
# =============================================================================
|
||||
|
||||
[[skills.config]]
|
||||
path = ".agents/skills/swarm-orchestration"
|
||||
enabled = true
|
||||
|
||||
[[skills.config]]
|
||||
path = ".agents/skills/memory-management"
|
||||
enabled = true
|
||||
|
||||
[[skills.config]]
|
||||
path = ".agents/skills/sparc-methodology"
|
||||
enabled = true
|
||||
|
||||
[[skills.config]]
|
||||
path = ".agents/skills/security-audit"
|
||||
enabled = true
|
||||
|
||||
# =============================================================================
|
||||
# Profiles
|
||||
# =============================================================================
|
||||
|
||||
# Development profile - more permissive for local work
|
||||
[profiles.dev]
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
web_search = "live"
|
||||
|
||||
# Safe profile - maximum restrictions
|
||||
[profiles.safe]
|
||||
approval_policy = "untrusted"
|
||||
sandbox_mode = "read-only"
|
||||
web_search = "disabled"
|
||||
|
||||
# CI profile - for automated pipelines
|
||||
[profiles.ci]
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "workspace-write"
|
||||
web_search = "cached"
|
||||
|
||||
# =============================================================================
|
||||
# History
|
||||
# =============================================================================
|
||||
|
||||
[history]
|
||||
# Save all session transcripts
|
||||
persistence = "save-all"
|
||||
|
||||
# =============================================================================
|
||||
# Shell Environment
|
||||
# =============================================================================
|
||||
|
||||
[shell_environment_policy]
|
||||
# Inherit environment variables
|
||||
inherit = "core"
|
||||
|
||||
# Exclude sensitive variables
|
||||
exclude = ["*_KEY", "*_SECRET", "*_TOKEN", "*_PASSWORD"]
|
||||
|
||||
# =============================================================================
|
||||
# Sandbox Workspace Write Settings
|
||||
# =============================================================================
|
||||
|
||||
[sandbox_workspace_write]
|
||||
# Additional writable paths beyond workspace
|
||||
writable_roots = []
|
||||
|
||||
# Allow network access
|
||||
network_access = true
|
||||
|
||||
# Exclude temp directories
|
||||
exclude_slash_tmp = false
|
||||
|
||||
# =============================================================================
|
||||
# Security Settings
|
||||
# =============================================================================
|
||||
|
||||
[security]
|
||||
# Enable input validation for all user inputs
|
||||
input_validation = true
|
||||
|
||||
# Prevent directory traversal attacks
|
||||
path_traversal_prevention = true
|
||||
|
||||
# Scan for hardcoded secrets
|
||||
secret_scanning = true
|
||||
|
||||
# Scan dependencies for known CVEs
|
||||
cve_scanning = true
|
||||
|
||||
# Maximum file size for operations (bytes)
|
||||
max_file_size = 10485760
|
||||
|
||||
# Allowed file extensions (empty = allow all)
|
||||
allowed_extensions = []
|
||||
|
||||
# Blocked file patterns (regex)
|
||||
blocked_patterns = ["\\.env$", "credentials\\.json$", "\\.pem$", "\\.key$"]
|
||||
|
||||
# =============================================================================
|
||||
# Performance Settings
|
||||
# =============================================================================
|
||||
|
||||
[performance]
|
||||
# Maximum concurrent agents
|
||||
max_agents = 8
|
||||
|
||||
# Task timeout in seconds
|
||||
task_timeout = 300
|
||||
|
||||
# Memory limit per agent
|
||||
memory_limit = "512MB"
|
||||
|
||||
# Enable response caching
|
||||
cache_enabled = true
|
||||
|
||||
# Cache TTL in seconds
|
||||
cache_ttl = 3600
|
||||
|
||||
# Enable parallel task execution
|
||||
parallel_execution = true
|
||||
|
||||
# =============================================================================
|
||||
# Logging Settings
|
||||
# =============================================================================
|
||||
|
||||
[logging]
|
||||
# Log level: debug, info, warn, error
|
||||
level = "info"
|
||||
|
||||
# Log format: json, text, pretty
|
||||
format = "pretty"
|
||||
|
||||
# Log destination: stdout, file, both
|
||||
destination = "stdout"
|
||||
|
||||
# =============================================================================
|
||||
# Neural Intelligence Settings
|
||||
# =============================================================================
|
||||
|
||||
[neural]
|
||||
# Enable SONA (Self-Optimizing Neural Architecture)
|
||||
sona_enabled = true
|
||||
|
||||
# Enable HNSW vector search
|
||||
hnsw_enabled = true
|
||||
|
||||
# HNSW index parameters
|
||||
hnsw_m = 16
|
||||
hnsw_ef_construction = 200
|
||||
hnsw_ef_search = 100
|
||||
|
||||
# Enable pattern learning
|
||||
pattern_learning = true
|
||||
|
||||
# Learning rate for neural adaptation
|
||||
learning_rate = 0.01
|
||||
|
||||
# =============================================================================
|
||||
# Swarm Orchestration Settings
|
||||
# =============================================================================
|
||||
|
||||
[swarm]
|
||||
# Default topology: hierarchical, mesh, ring, star
|
||||
default_topology = "hierarchical"
|
||||
|
||||
# Default strategy: balanced, specialized, adaptive
|
||||
default_strategy = "specialized"
|
||||
|
||||
# Consensus algorithm: raft, byzantine, gossip
|
||||
consensus = "raft"
|
||||
|
||||
# Enable anti-drift measures
|
||||
anti_drift = true
|
||||
|
||||
# Checkpoint interval (tasks)
|
||||
checkpoint_interval = 10
|
||||
|
||||
# =============================================================================
|
||||
# Hooks Configuration
|
||||
# =============================================================================
|
||||
|
||||
[hooks]
|
||||
# Enable lifecycle hooks
|
||||
enabled = true
|
||||
|
||||
# Pre-task hook
|
||||
pre_task = true
|
||||
|
||||
# Post-task hook (for learning)
|
||||
post_task = true
|
||||
|
||||
# Enable neural training on post-edit
|
||||
train_on_edit = true
|
||||
|
||||
# =============================================================================
|
||||
# Background Workers
|
||||
# =============================================================================
|
||||
|
||||
[workers]
|
||||
# Enable background workers
|
||||
enabled = true
|
||||
|
||||
# Worker configuration
|
||||
[workers.audit]
|
||||
enabled = true
|
||||
priority = "critical"
|
||||
interval = 300
|
||||
|
||||
[workers.optimize]
|
||||
enabled = true
|
||||
priority = "high"
|
||||
interval = 600
|
||||
|
||||
[workers.consolidate]
|
||||
enabled = true
|
||||
priority = "low"
|
||||
interval = 1800
|
||||
@ -1,126 +0,0 @@
|
||||
---
|
||||
name: memory-management
|
||||
description: >
|
||||
AgentDB memory system with HNSW vector search. Provides 150x-12,500x faster pattern retrieval, persistent storage, and semantic search capabilities for learning and knowledge management.
|
||||
Use when: need to store successful patterns, searching for similar solutions, semantic lookup of past work, learning from previous tasks, sharing knowledge between agents, building knowledge base.
|
||||
Skip when: no learning needed, ephemeral one-off tasks, external data sources available, read-only exploration.
|
||||
---
|
||||
|
||||
# Memory Management Skill
|
||||
|
||||
## Purpose
|
||||
AgentDB memory system with HNSW vector search. Provides 150x-12,500x faster pattern retrieval, persistent storage, and semantic search capabilities for learning and knowledge management.
|
||||
|
||||
## When to Trigger
|
||||
- need to store successful patterns
|
||||
- searching for similar solutions
|
||||
- semantic lookup of past work
|
||||
- learning from previous tasks
|
||||
- sharing knowledge between agents
|
||||
- building knowledge base
|
||||
|
||||
## When to Skip
|
||||
- no learning needed
|
||||
- ephemeral one-off tasks
|
||||
- external data sources available
|
||||
- read-only exploration
|
||||
|
||||
## Commands
|
||||
|
||||
### Store Pattern
|
||||
Store a pattern or knowledge item in memory
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli memory store --key "[key]" --value "[value]" --namespace patterns
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli memory store --key "auth-jwt-pattern" --value "JWT validation with refresh tokens" --namespace patterns
|
||||
```
|
||||
|
||||
### Semantic Search
|
||||
Search memory using semantic similarity
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli memory search --query "[search terms]" --limit 10
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli memory search --query "authentication best practices" --limit 5
|
||||
```
|
||||
|
||||
### Retrieve Entry
|
||||
Retrieve a specific memory entry by key
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli memory get --key "[key]" --namespace [namespace]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli memory get --key "auth-jwt-pattern" --namespace patterns
|
||||
```
|
||||
|
||||
### List Entries
|
||||
List all entries in a namespace
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli memory list --namespace [namespace]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli memory list --namespace patterns --limit 20
|
||||
```
|
||||
|
||||
### Delete Entry
|
||||
Delete a memory entry
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli memory delete --key "[key]" --namespace [namespace]
|
||||
```
|
||||
|
||||
### Initialize HNSW Index
|
||||
Initialize HNSW vector search index
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli memory init --enable-hnsw
|
||||
```
|
||||
|
||||
### Memory Stats
|
||||
Show memory usage statistics
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli memory stats
|
||||
```
|
||||
|
||||
### Export Memory
|
||||
Export memory to JSON
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli memory export --output memory-backup.json
|
||||
```
|
||||
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `memory-backup` | `.agents/scripts/memory-backup.sh` | Backup memory to external storage |
|
||||
| `memory-consolidate` | `.agents/scripts/memory-consolidate.sh` | Consolidate and optimize memory |
|
||||
|
||||
|
||||
## References
|
||||
|
||||
| Document | Path | Description |
|
||||
|----------|------|-------------|
|
||||
| `HNSW Guide` | `docs/hnsw.md` | HNSW vector search configuration |
|
||||
| `Memory Schema` | `docs/memory-schema.md` | Memory namespace and schema reference |
|
||||
|
||||
## Best Practices
|
||||
1. Check memory for existing patterns before starting
|
||||
2. Use hierarchical topology for coordination
|
||||
3. Store successful patterns after completion
|
||||
4. Document any new learnings
|
||||
@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Memory Management - Backup Script
|
||||
# Export memory to backup file
|
||||
|
||||
set -e
|
||||
|
||||
BACKUP_DIR="${BACKUP_DIR:-./.backups}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="${BACKUP_DIR}/memory_${TIMESTAMP}.json"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
echo "Backing up memory to $BACKUP_FILE..."
|
||||
npx @claude-flow/cli memory export --output "$BACKUP_FILE"
|
||||
|
||||
echo "Backup complete: $BACKUP_FILE"
|
||||
@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Memory Management - Consolidate Script
|
||||
# Optimize and consolidate memory
|
||||
|
||||
set -e
|
||||
|
||||
echo "Running memory consolidation..."
|
||||
npx @claude-flow/cli hooks worker dispatch --trigger consolidate
|
||||
|
||||
echo "Memory consolidation complete"
|
||||
npx @claude-flow/cli memory stats
|
||||
@ -1,135 +0,0 @@
|
||||
---
|
||||
name: security-audit
|
||||
description: >
|
||||
Comprehensive security scanning and vulnerability detection. Includes input validation, path traversal prevention, CVE detection, and secure coding pattern enforcement.
|
||||
Use when: authentication implementation, authorization logic, payment processing, user data handling, API endpoint creation, file upload handling, database queries, external API integration.
|
||||
Skip when: read-only operations on public data, internal development tooling, static documentation, styling changes.
|
||||
---
|
||||
|
||||
# Security Audit Skill
|
||||
|
||||
## Purpose
|
||||
Comprehensive security scanning and vulnerability detection. Includes input validation, path traversal prevention, CVE detection, and secure coding pattern enforcement.
|
||||
|
||||
## When to Trigger
|
||||
- authentication implementation
|
||||
- authorization logic
|
||||
- payment processing
|
||||
- user data handling
|
||||
- API endpoint creation
|
||||
- file upload handling
|
||||
- database queries
|
||||
- external API integration
|
||||
|
||||
## When to Skip
|
||||
- read-only operations on public data
|
||||
- internal development tooling
|
||||
- static documentation
|
||||
- styling changes
|
||||
|
||||
## Commands
|
||||
|
||||
### Full Security Scan
|
||||
Run comprehensive security analysis on the codebase
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security scan --depth full
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli security scan --depth full --output security-report.json
|
||||
```
|
||||
|
||||
### Input Validation Check
|
||||
Check for input validation issues
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security scan --check input-validation
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli security scan --check input-validation --path ./src/api
|
||||
```
|
||||
|
||||
### Path Traversal Check
|
||||
Check for path traversal vulnerabilities
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security scan --check path-traversal
|
||||
```
|
||||
|
||||
### SQL Injection Check
|
||||
Check for SQL injection vulnerabilities
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security scan --check sql-injection
|
||||
```
|
||||
|
||||
### XSS Check
|
||||
Check for cross-site scripting vulnerabilities
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security scan --check xss
|
||||
```
|
||||
|
||||
### CVE Scan
|
||||
Scan dependencies for known CVEs
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security cve --scan
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli security cve --scan --severity high
|
||||
```
|
||||
|
||||
### Security Audit Report
|
||||
Generate full security audit report
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security audit --report
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli security audit --report --format markdown --output SECURITY.md
|
||||
```
|
||||
|
||||
### Threat Modeling
|
||||
Run threat modeling analysis
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security threats --analyze
|
||||
```
|
||||
|
||||
### Validate Secrets
|
||||
Check for hardcoded secrets
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli security validate --check secrets
|
||||
```
|
||||
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `security-scan` | `.agents/scripts/security-scan.sh` | Run full security scan pipeline |
|
||||
| `cve-remediate` | `.agents/scripts/cve-remediate.sh` | Auto-remediate known CVEs |
|
||||
|
||||
|
||||
## References
|
||||
|
||||
| Document | Path | Description |
|
||||
|----------|------|-------------|
|
||||
| `Security Checklist` | `docs/security-checklist.md` | Security review checklist |
|
||||
| `OWASP Guide` | `docs/owasp-top10.md` | OWASP Top 10 mitigation guide |
|
||||
|
||||
## Best Practices
|
||||
1. Check memory for existing patterns before starting
|
||||
2. Use hierarchical topology for coordination
|
||||
3. Store successful patterns after completion
|
||||
4. Document any new learnings
|
||||
@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Security Audit - CVE Remediation Script
|
||||
# Auto-remediate known CVEs
|
||||
|
||||
set -e
|
||||
|
||||
echo "Scanning for CVEs..."
|
||||
npx @claude-flow/cli security cve --scan --severity high
|
||||
|
||||
echo "Attempting auto-remediation..."
|
||||
npm audit fix
|
||||
|
||||
echo "Re-scanning after remediation..."
|
||||
npx @claude-flow/cli security cve --scan
|
||||
|
||||
echo "CVE remediation complete"
|
||||
@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Security Audit - Full Scan Script
|
||||
# Run comprehensive security scan pipeline
|
||||
|
||||
set -e
|
||||
|
||||
echo "Running full security scan..."
|
||||
|
||||
# Input validation
|
||||
echo "Checking input validation..."
|
||||
npx @claude-flow/cli security scan --check input-validation
|
||||
|
||||
# Path traversal
|
||||
echo "Checking path traversal..."
|
||||
npx @claude-flow/cli security scan --check path-traversal
|
||||
|
||||
# SQL injection
|
||||
echo "Checking SQL injection..."
|
||||
npx @claude-flow/cli security scan --check sql-injection
|
||||
|
||||
# XSS
|
||||
echo "Checking XSS..."
|
||||
npx @claude-flow/cli security scan --check xss
|
||||
|
||||
# Secrets
|
||||
echo "Checking for hardcoded secrets..."
|
||||
npx @claude-flow/cli security validate --check secrets
|
||||
|
||||
# CVE scan
|
||||
echo "Scanning dependencies for CVEs..."
|
||||
npx @claude-flow/cli security cve --scan
|
||||
|
||||
echo "Security scan complete"
|
||||
@ -1,118 +0,0 @@
|
||||
---
|
||||
name: sparc-methodology
|
||||
description: >
|
||||
SPARC development workflow: Specification, Pseudocode, Architecture, Refinement, Completion. A structured approach for complex implementations that ensures thorough planning before coding.
|
||||
Use when: new feature implementation, complex implementations, architectural changes, system redesign, integration work, unclear requirements.
|
||||
Skip when: simple bug fixes, documentation updates, configuration changes, well-defined small tasks, routine maintenance.
|
||||
---
|
||||
|
||||
# Sparc Methodology Skill
|
||||
|
||||
## Purpose
|
||||
SPARC development workflow: Specification, Pseudocode, Architecture, Refinement, Completion. A structured approach for complex implementations that ensures thorough planning before coding.
|
||||
|
||||
## When to Trigger
|
||||
- new feature implementation
|
||||
- complex implementations
|
||||
- architectural changes
|
||||
- system redesign
|
||||
- integration work
|
||||
- unclear requirements
|
||||
|
||||
## When to Skip
|
||||
- simple bug fixes
|
||||
- documentation updates
|
||||
- configuration changes
|
||||
- well-defined small tasks
|
||||
- routine maintenance
|
||||
|
||||
## Commands
|
||||
|
||||
### Specification Phase
|
||||
Define requirements, acceptance criteria, and constraints
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "specification: [requirements]"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "specification: user authentication with OAuth2, MFA, and session management"
|
||||
```
|
||||
|
||||
### Pseudocode Phase
|
||||
Write high-level pseudocode for the implementation
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "pseudocode: [feature]"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "pseudocode: OAuth2 login flow with token refresh"
|
||||
```
|
||||
|
||||
### Architecture Phase
|
||||
Design system structure, interfaces, and dependencies
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "architecture: [design]"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "architecture: auth module with service layer, repository, and API endpoints"
|
||||
```
|
||||
|
||||
### Refinement Phase
|
||||
Iterate on the design based on feedback
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "refinement: [feedback]"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "refinement: add rate limiting and brute force protection"
|
||||
```
|
||||
|
||||
### Completion Phase
|
||||
Finalize implementation with tests and documentation
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "completion: [final checks]"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "completion: verify all tests pass, update API docs, security review"
|
||||
```
|
||||
|
||||
### SPARC Coordinator
|
||||
Spawn SPARC coordinator agent
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli agent spawn --type sparc-coord --name sparc-lead
|
||||
```
|
||||
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `sparc-init` | `.agents/scripts/sparc-init.sh` | Initialize SPARC workflow for a new feature |
|
||||
| `sparc-review` | `.agents/scripts/sparc-review.sh` | Run SPARC phase review checklist |
|
||||
|
||||
|
||||
## References
|
||||
|
||||
| Document | Path | Description |
|
||||
|----------|------|-------------|
|
||||
| `SPARC Overview` | `docs/sparc.md` | Complete SPARC methodology guide |
|
||||
| `Phase Templates` | `docs/sparc-templates.md` | Templates for each SPARC phase |
|
||||
|
||||
## Best Practices
|
||||
1. Check memory for existing patterns before starting
|
||||
2. Use hierarchical topology for coordination
|
||||
3. Store successful patterns after completion
|
||||
4. Document any new learnings
|
||||
@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
# SPARC Methodology - Init Script
|
||||
# Initialize SPARC workflow for a new feature
|
||||
|
||||
set -e
|
||||
|
||||
FEATURE_NAME="${1:-new-feature}"
|
||||
|
||||
echo "Initializing SPARC workflow for: $FEATURE_NAME"
|
||||
|
||||
# Create SPARC documentation directory
|
||||
mkdir -p "./docs/sparc/$FEATURE_NAME"
|
||||
|
||||
# Create phase files
|
||||
touch "./docs/sparc/$FEATURE_NAME/1-specification.md"
|
||||
touch "./docs/sparc/$FEATURE_NAME/2-pseudocode.md"
|
||||
touch "./docs/sparc/$FEATURE_NAME/3-architecture.md"
|
||||
touch "./docs/sparc/$FEATURE_NAME/4-refinement.md"
|
||||
touch "./docs/sparc/$FEATURE_NAME/5-completion.md"
|
||||
|
||||
echo "SPARC workflow initialized in ./docs/sparc/$FEATURE_NAME"
|
||||
@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
# SPARC Methodology - Review Script
|
||||
# Run SPARC phase review checklist
|
||||
|
||||
set -e
|
||||
|
||||
FEATURE_DIR="${1:-.}"
|
||||
|
||||
echo "SPARC Phase Review Checklist"
|
||||
echo "============================="
|
||||
|
||||
for phase in specification pseudocode architecture refinement completion; do
|
||||
if [ -f "$FEATURE_DIR/${phase}.md" ]; then
|
||||
echo "[x] $phase - found"
|
||||
else
|
||||
echo "[ ] $phase - missing"
|
||||
fi
|
||||
done
|
||||
@ -1,114 +0,0 @@
|
||||
---
|
||||
name: swarm-orchestration
|
||||
description: >
|
||||
Multi-agent swarm coordination for complex tasks. Uses hierarchical topology with specialized agents to break down and execute complex work across multiple files and modules.
|
||||
Use when: 3+ files need changes, new feature implementation, cross-module refactoring, API changes with tests, security-related changes, performance optimization across codebase, database schema changes.
|
||||
Skip when: single file edits, simple bug fixes (1-2 lines), documentation updates, configuration changes, quick exploration.
|
||||
---
|
||||
|
||||
# Swarm Orchestration Skill
|
||||
|
||||
## Purpose
|
||||
Multi-agent swarm coordination for complex tasks. Uses hierarchical topology with specialized agents to break down and execute complex work across multiple files and modules.
|
||||
|
||||
## When to Trigger
|
||||
- 3+ files need changes
|
||||
- new feature implementation
|
||||
- cross-module refactoring
|
||||
- API changes with tests
|
||||
- security-related changes
|
||||
- performance optimization across codebase
|
||||
- database schema changes
|
||||
|
||||
## When to Skip
|
||||
- single file edits
|
||||
- simple bug fixes (1-2 lines)
|
||||
- documentation updates
|
||||
- configuration changes
|
||||
- quick exploration
|
||||
|
||||
## Commands
|
||||
|
||||
### Initialize Swarm
|
||||
Start a new swarm with hierarchical topology (anti-drift)
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli swarm init --topology hierarchical --max-agents 8 --strategy specialized
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli swarm init --topology hierarchical --max-agents 6 --strategy specialized
|
||||
```
|
||||
|
||||
### Route Task
|
||||
Route a task to the appropriate agents based on task type
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "[task description]"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli hooks route --task "implement OAuth2 authentication flow"
|
||||
```
|
||||
|
||||
### Spawn Agent
|
||||
Spawn a specific agent type
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli agent spawn --type [type] --name [name]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli agent spawn --type coder --name impl-auth
|
||||
```
|
||||
|
||||
### Monitor Status
|
||||
Check the current swarm status
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli swarm status --verbose
|
||||
```
|
||||
|
||||
### Orchestrate Task
|
||||
Orchestrate a task across multiple agents
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli task orchestrate --task "[task]" --strategy adaptive
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx @claude-flow/cli task orchestrate --task "refactor auth module" --strategy parallel --max-agents 4
|
||||
```
|
||||
|
||||
### List Agents
|
||||
List all active agents
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli agent list --filter active
|
||||
```
|
||||
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `swarm-start` | `.agents/scripts/swarm-start.sh` | Initialize swarm with default settings |
|
||||
| `swarm-monitor` | `.agents/scripts/swarm-monitor.sh` | Real-time swarm monitoring dashboard |
|
||||
|
||||
|
||||
## References
|
||||
|
||||
| Document | Path | Description |
|
||||
|----------|------|-------------|
|
||||
| `Agent Types` | `docs/agents.md` | Complete list of agent types and capabilities |
|
||||
| `Topology Guide` | `docs/topology.md` | Swarm topology configuration guide |
|
||||
|
||||
## Best Practices
|
||||
1. Check memory for existing patterns before starting
|
||||
2. Use hierarchical topology for coordination
|
||||
3. Store successful patterns after completion
|
||||
4. Document any new learnings
|
||||
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Swarm Orchestration - Monitor Script
|
||||
# Real-time swarm monitoring
|
||||
|
||||
set -e
|
||||
|
||||
echo "Starting swarm monitor..."
|
||||
npx @claude-flow/cli swarm status --watch --interval 5
|
||||
@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Swarm Orchestration - Start Script
|
||||
# Initialize swarm with default anti-drift settings
|
||||
|
||||
set -e
|
||||
|
||||
echo "Initializing hierarchical swarm..."
|
||||
npx @claude-flow/cli swarm init \
|
||||
--topology hierarchical \
|
||||
--max-agents 8 \
|
||||
--strategy specialized
|
||||
|
||||
echo "Swarm initialized successfully"
|
||||
npx @claude-flow/cli swarm status
|
||||
@ -1,144 +0,0 @@
|
||||
{
|
||||
"tasks": {
|
||||
"task-1773164482180-yqy5i7": {
|
||||
"taskId": "task-1773164482180-yqy5i7",
|
||||
"type": "feature",
|
||||
"description": "Add PkgID field to OrderRemark struct and parse pkg_id:XX in remark.Parse()",
|
||||
"priority": "high",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"assignedTo": [],
|
||||
"tags": [
|
||||
"remark",
|
||||
"parsing",
|
||||
"ichiban"
|
||||
],
|
||||
"createdAt": "2026-03-10T17:41:22.180Z",
|
||||
"startedAt": null,
|
||||
"completedAt": "2026-03-10T17:41:55.018Z",
|
||||
"result": {
|
||||
"reason": "Added PkgID field to OrderRemark struct and pkg_id: parsing branch in Parse()"
|
||||
}
|
||||
},
|
||||
"task-1773164482201-s9jemx": {
|
||||
"taskId": "task-1773164482201-s9jemx",
|
||||
"type": "feature",
|
||||
"description": "Extend calcPaidByPriceDraw with three-way classification: Case1 ActivityID>0 (lottery), Case2 IssueID>0 (matching game via activity_issues), Case3 PkgID>0 (ichiban via game_pass_packages)",
|
||||
"priority": "high",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"assignedTo": [],
|
||||
"tags": [
|
||||
"channel-stats",
|
||||
"matching-game",
|
||||
"ichiban",
|
||||
"calcPaidByPriceDraw"
|
||||
],
|
||||
"createdAt": "2026-03-10T17:41:22.201Z",
|
||||
"startedAt": null,
|
||||
"completedAt": "2026-03-10T17:42:34.567Z",
|
||||
"result": {
|
||||
"reason": "Extended calcPaidByPriceDraw with three-way classification: lottery (ActivityID), matching game (IssueID→activity_issues→activities), ichiban (PkgID→game_pass_packages)"
|
||||
}
|
||||
},
|
||||
"task-1773164482206-mhmqsb": {
|
||||
"taskId": "task-1773164482206-mhmqsb",
|
||||
"type": "feature",
|
||||
"description": "Build verification: run make build-mac to ensure compilation passes after changes",
|
||||
"priority": "normal",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"assignedTo": [],
|
||||
"tags": [
|
||||
"build",
|
||||
"verification"
|
||||
],
|
||||
"createdAt": "2026-03-10T17:41:22.206Z",
|
||||
"startedAt": null,
|
||||
"completedAt": "2026-03-10T17:43:27.419Z",
|
||||
"result": {
|
||||
"reason": "Build passed successfully on macOS"
|
||||
}
|
||||
},
|
||||
"task-1773166041411-fmshox": {
|
||||
"taskId": "task-1773166041411-fmshox",
|
||||
"type": "feature",
|
||||
"description": "Extend StatsOverview and StatsDailyItem structs with cost_cents, profit_cents, total_cost, total_profit fields",
|
||||
"priority": "high",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"assignedTo": [],
|
||||
"tags": [
|
||||
"channel-stats",
|
||||
"profit-loss",
|
||||
"structs"
|
||||
],
|
||||
"createdAt": "2026-03-10T18:07:21.411Z",
|
||||
"startedAt": null,
|
||||
"completedAt": "2026-03-10T18:07:47.297Z",
|
||||
"result": {
|
||||
"reason": "Extended StatsOverview with TotalCostCents/TotalProfitCents/TotalCost/TotalProfit and StatsDailyItem with CostCents/ProfitCents"
|
||||
}
|
||||
},
|
||||
"task-1773166041417-di6rsd": {
|
||||
"taskId": "task-1773166041417-di6rsd",
|
||||
"type": "feature",
|
||||
"description": "Implement calcCostByInventory helper function: query user_inventory with item card multiplier, grouped by date",
|
||||
"priority": "high",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"assignedTo": [],
|
||||
"tags": [
|
||||
"channel-stats",
|
||||
"profit-loss",
|
||||
"cost-calculation"
|
||||
],
|
||||
"createdAt": "2026-03-10T18:07:21.417Z",
|
||||
"startedAt": null,
|
||||
"completedAt": "2026-03-10T18:08:23.196Z",
|
||||
"result": {
|
||||
"reason": "Implemented calcCostByInventory with 6-table JOIN chain, item card multiplier, COALESCE fallback, and optional date range"
|
||||
}
|
||||
},
|
||||
"task-1773166041422-efwp8w": {
|
||||
"taskId": "task-1773166041422-efwp8w",
|
||||
"type": "feature",
|
||||
"description": "Integrate calcCostByInventory into GetStats: Overview all-time cost + daily trend cost/profit",
|
||||
"priority": "high",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"assignedTo": [],
|
||||
"tags": [
|
||||
"channel-stats",
|
||||
"profit-loss",
|
||||
"integration"
|
||||
],
|
||||
"createdAt": "2026-03-10T18:07:21.422Z",
|
||||
"startedAt": null,
|
||||
"completedAt": "2026-03-10T18:09:08.231Z",
|
||||
"result": {
|
||||
"reason": "Integrated calcCostByInventory into GetStats: Overview all-time cost/profit + daily trend cost/profit"
|
||||
}
|
||||
},
|
||||
"task-1773166041428-fwp50t": {
|
||||
"taskId": "task-1773166041428-fwp50t",
|
||||
"type": "feature",
|
||||
"description": "Build verification and integration test against dev_game database",
|
||||
"priority": "normal",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"assignedTo": [],
|
||||
"tags": [
|
||||
"build",
|
||||
"verification"
|
||||
],
|
||||
"createdAt": "2026-03-10T18:07:21.428Z",
|
||||
"startedAt": null,
|
||||
"completedAt": "2026-03-10T18:10:06.759Z",
|
||||
"result": {
|
||||
"reason": "Build passed. Integration test shows: 754 inventory records, 2 with double card (双倍快乐水 ×2.0), cost=5100.50元, revenue(actual_amount)=545.00元"
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
@ -1,249 +0,0 @@
|
||||
# 渠道统计 — 前端盈亏展示
|
||||
|
||||
## 📋 实施计划:渠道统计页面新增成本/盈亏展示
|
||||
|
||||
### 任务类型
|
||||
- [x] 前端
|
||||
- [ ] 后端
|
||||
- [ ] 全栈
|
||||
|
||||
### 需求概述
|
||||
|
||||
后端 `/admin/channels/:id/stats` 接口已新增以下字段:
|
||||
|
||||
**Overview 新增**:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `total_cost_cents` | number | 总成本(分) |
|
||||
| `total_profit_cents` | number | 盈亏(分) = paid - cost |
|
||||
| `total_cost` | number | 总成本(元) |
|
||||
| `total_profit` | number | 盈亏(元) |
|
||||
|
||||
**趋势图每日新增**:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `cost_cents` | number | 当日成本(分) |
|
||||
| `profit_cents` | number | 当日盈亏(分) |
|
||||
|
||||
### 技术方案
|
||||
|
||||
#### UI 设计
|
||||
|
||||
**Overview 区域**:
|
||||
- 现有 3 个卡片(用户、订单、实付金额)→ 扩展为 **5 个卡片**
|
||||
- 新增:**总成本** 卡片 + **盈亏** 卡片
|
||||
- 布局:从 `grid-cols-3` 改为 `grid-cols-5`(或在移动端自适应 `grid-cols-2 md:grid-cols-5`)
|
||||
- 盈亏卡片需根据正/负值显示不同颜色(盈利=绿色,亏损=红色)
|
||||
|
||||
**趋势图区域**:
|
||||
- 现有 2 个 Tab(用户增长、付费数据)→ 新增第 3 个 Tab:**盈亏分析**
|
||||
- 盈亏分析 Tab 包含 3 条曲线:实付金额、成本、盈亏
|
||||
- 盈亏曲线可使用虚线区分
|
||||
|
||||
### 实施步骤
|
||||
|
||||
#### Step 1: 更新 TypeScript 类型定义
|
||||
|
||||
**文件**:`web/admin/src/api/channels.ts`
|
||||
|
||||
在 `StatsOverview` 接口新增:
|
||||
```typescript
|
||||
export interface StatsOverview {
|
||||
total_users: number
|
||||
total_orders: number
|
||||
total_gmv: number
|
||||
total_paid_cents?: number
|
||||
// 新增
|
||||
total_cost_cents?: number // 总成本(分)
|
||||
total_profit_cents?: number // 盈亏(分)
|
||||
total_cost?: number // 总成本(元)
|
||||
total_profit?: number // 盈亏(元)
|
||||
}
|
||||
```
|
||||
|
||||
在 `StatsDailyItem` 接口新增:
|
||||
```typescript
|
||||
export interface StatsDailyItem {
|
||||
date: string
|
||||
user_count: number
|
||||
order_count: number
|
||||
gmv: number
|
||||
paid_cents?: number
|
||||
// 新增
|
||||
cost_cents?: number // 当日成本(分)
|
||||
profit_cents?: number // 当日盈亏(分)
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: 更新 Overview 卡片区域
|
||||
|
||||
**文件**:`web/admin/src/views/operations/channels/index.vue`
|
||||
|
||||
**2.1** 布局从 `grid-cols-3` 改为 `grid-cols-5`
|
||||
|
||||
**2.2** 新增两个 `ArtStatsCard`:
|
||||
|
||||
```vue
|
||||
<!-- 总成本 -->
|
||||
<ArtStatsCard
|
||||
title="总成本"
|
||||
:count="totalCostYuan"
|
||||
:decimals="2"
|
||||
icon="ri:funds-line"
|
||||
box-style="bg-purple-50"
|
||||
text-color="#7C3AED"
|
||||
icon-style="bg-purple-500"
|
||||
description="总奖品成本"
|
||||
/>
|
||||
|
||||
<!-- 盈亏 -->
|
||||
<ArtStatsCard
|
||||
title="盈亏"
|
||||
:count="totalProfitYuan"
|
||||
:decimals="2"
|
||||
icon="ri:bar-chart-2-line"
|
||||
:box-style="profitCardStyle"
|
||||
:text-color="profitTextColor"
|
||||
:icon-style="profitIconStyle"
|
||||
:description="profitDescription"
|
||||
/>
|
||||
```
|
||||
|
||||
**2.3** 新增 computed 属性:
|
||||
|
||||
```typescript
|
||||
const totalCostYuan = computed(() => {
|
||||
const cents = statsData.value.overview.total_cost_cents
|
||||
if (typeof cents === 'number') {
|
||||
return Number((cents / 100).toFixed(2))
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const totalProfitYuan = computed(() => {
|
||||
const cents = statsData.value.overview.total_profit_cents
|
||||
if (typeof cents === 'number') {
|
||||
return Number((cents / 100).toFixed(2))
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const profitCardStyle = computed(() =>
|
||||
totalProfitYuan.value >= 0 ? 'bg-green-50' : 'bg-red-50'
|
||||
)
|
||||
|
||||
const profitTextColor = computed(() =>
|
||||
totalProfitYuan.value >= 0 ? '#10B981' : '#EF4444'
|
||||
)
|
||||
|
||||
const profitIconStyle = computed(() =>
|
||||
totalProfitYuan.value >= 0 ? 'bg-green-500' : 'bg-red-500'
|
||||
)
|
||||
|
||||
const profitDescription = computed(() =>
|
||||
totalProfitYuan.value >= 0 ? '盈利' : '亏损'
|
||||
)
|
||||
```
|
||||
|
||||
#### Step 3: 更新趋势图 Tab
|
||||
|
||||
**文件**:`web/admin/src/views/operations/channels/index.vue`
|
||||
|
||||
**3.1** 在 `el-radio-group` 新增 Tab:
|
||||
|
||||
```vue
|
||||
<el-radio-group v-model="statsTab" size="small">
|
||||
<el-radio-button label="growth">用户增长</el-radio-button>
|
||||
<el-radio-button label="revenue">付费数据</el-radio-button>
|
||||
<el-radio-button label="profit">盈亏分析</el-radio-button>
|
||||
</el-radio-group>
|
||||
```
|
||||
|
||||
**3.2** 在 `chartData` computed 中新增 `profit` 分支:
|
||||
|
||||
```typescript
|
||||
const chartData = computed(() => {
|
||||
if (statsTab.value === 'growth') {
|
||||
return [
|
||||
{ name: '新增用户', data: statsData.value.daily.map(i => i.user_count), smooth: true, color: '#409EFF' }
|
||||
]
|
||||
} else if (statsTab.value === 'revenue') {
|
||||
return [
|
||||
{ name: '订单数', data: statsData.value.daily.map(i => i.order_count), smooth: true, color: '#67C23A' },
|
||||
{ name: '实付金额', data: statsData.value.daily.map(i => getDailyPaidYuan(i)), smooth: true, color: '#E6A23C' }
|
||||
]
|
||||
} else {
|
||||
// profit tab
|
||||
return [
|
||||
{ name: '实付(元)', data: statsData.value.daily.map(i => getDailyPaidYuan(i)), smooth: true, color: '#E6A23C' },
|
||||
{ name: '成本(元)', data: statsData.value.daily.map(i => getDailyCostYuan(i)), smooth: true, color: '#7C3AED' },
|
||||
{ name: '盈亏(元)', data: statsData.value.daily.map(i => getDailyProfitYuan(i)), smooth: true, color: '#10B981' }
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**3.3** 新增辅助函数:
|
||||
|
||||
```typescript
|
||||
function getDailyCostYuan(item: { cost_cents?: number }) {
|
||||
if (typeof item.cost_cents === 'number') {
|
||||
return Number((item.cost_cents / 100).toFixed(2))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function getDailyProfitYuan(item: { profit_cents?: number }) {
|
||||
if (typeof item.profit_cents === 'number') {
|
||||
return Number((item.profit_cents / 100).toFixed(2))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4: 更新 statsData 初始值
|
||||
|
||||
**文件**:`web/admin/src/views/operations/channels/index.vue`
|
||||
|
||||
```typescript
|
||||
const statsData = ref<ChannelStatsRes>({
|
||||
overview: {
|
||||
total_users: 0, total_orders: 0, total_gmv: 0, total_paid_cents: 0,
|
||||
total_cost_cents: 0, total_profit_cents: 0, total_cost: 0, total_profit: 0
|
||||
},
|
||||
daily: []
|
||||
})
|
||||
```
|
||||
|
||||
### 关键文件
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `web/admin/src/api/channels.ts:L68-86` | 修改 | 扩展 StatsOverview 和 StatsDailyItem 接口 |
|
||||
| `web/admin/src/views/operations/channels/index.vue:L155-184` | 修改 | Overview 卡片区域新增成本/盈亏卡 |
|
||||
| `web/admin/src/views/operations/channels/index.vue:L202-205` | 修改 | 趋势图新增盈亏分析 Tab |
|
||||
| `web/admin/src/views/operations/channels/index.vue:L482-558` | 修改 | 新增 computed 属性和辅助函数 |
|
||||
|
||||
### 风险与缓解
|
||||
|
||||
| 风险 | 严重程度 | 缓解措施 |
|
||||
|------|---------|----------|
|
||||
| 5列卡片在窄屏溢出 | 低 | 使用响应式 `grid-cols-2 md:grid-cols-5`,必要时改为 `grid-cols-3` + 第二行 `grid-cols-2` |
|
||||
| 后端字段为空(旧数据) | 已解决 | 所有新字段使用 `?` 可选,computed 中做 `typeof` 检查,默认 0 |
|
||||
| ArtStatsCard 不支持负数展示 | 低 | ArtCountTo 组件底层支持负数(基于 countUp.js),无需额外处理 |
|
||||
| 盈亏曲线可能有负值 | 低 | ECharts 原生支持负值 Y 轴,图表会自动适配 |
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] TypeScript 类型定义包含新字段
|
||||
- [ ] Overview 展示 5 个卡片(用户、订单、实付、成本、盈亏)
|
||||
- [ ] 盈亏卡片根据正/负值动态切换颜色(绿/红)
|
||||
- [ ] 趋势图新增"盈亏分析"Tab
|
||||
- [ ] 盈亏分析 Tab 展示 3 条曲线(实付、成本、盈亏)
|
||||
- [ ] `pnpm build` 编译通过
|
||||
- [ ] `pnpm type-check` 类型检查通过
|
||||
|
||||
### SESSION_ID(供 /ccg:execute 使用)
|
||||
|
||||
- CODEX_SESSION: N/A
|
||||
- GEMINI_SESSION: N/A
|
||||
@ -1,311 +0,0 @@
|
||||
# 渠道统计接口优化计划
|
||||
|
||||
## 需求概述
|
||||
|
||||
优化 `/admin/channels/:channel_id/stats` 接口:
|
||||
|
||||
| 指标 | 当前实现 | 优化后 |
|
||||
|------|---------|--------|
|
||||
| 累计用户 | `COUNT(users WHERE channel_id = X)` | 保持不变 — **全量统计,不限时间** |
|
||||
| 累计订单 | `COUNT(orders JOIN users ...)` | 保持不变 — **全量统计,不限时间** |
|
||||
| 累计实付金额 | `SUM(orders.actual_amount)` | remark → activityID → `activities.price_draw × count` — **全量统计,不限时间** |
|
||||
| 趋势图表 | 按**月**分组(`days` 参数实际当月用) | 修正为按**天**分组,`days` 参数控制天数范围 |
|
||||
|
||||
## 确认的决策
|
||||
|
||||
- ✅ 直接用 remark 中 activityID 查 `activities.price_draw`
|
||||
- ✅ 软删除活动也计入(使用 `Unscoped`)
|
||||
- ✅ `days` 参数修正为按天计算
|
||||
- ✅ Overview 三个指标为全量(不受 days 限制)
|
||||
|
||||
## 受影响的代码
|
||||
|
||||
| 方法 | 文件 | 行号 | 改动内容 |
|
||||
|------|------|------|---------|
|
||||
| `GetStats` | `internal/service/channel/channel.go` | L238-355 | 核心改动:金额计算 + days 修正 + 按天分组 |
|
||||
| `List` | `internal/service/channel/channel.go` | L157-236 | 同步改动:列表 paid_amount 用 price_draw 计算 |
|
||||
| `StatsOutput` / `StatsDailyItem` | `internal/service/channel/channel.go` | L66-84 | 结构体不变,`Daily` 改为按天粒度 |
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Step 1: 新增 `orderRemarkRow` 类型和 `calcPaidByPriceDraw` 辅助函数
|
||||
|
||||
**文件**:`internal/service/channel/channel.go`
|
||||
|
||||
```go
|
||||
type orderRemarkRow struct {
|
||||
Remark string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// calcPaidByPriceDraw 解析订单 remark 中的 activityID + count,
|
||||
// 批量查 activities.price_draw(含软删除),计算实付金额
|
||||
// 返回:总金额(分)、按日期key分组的金额
|
||||
func (s *service) calcPaidByPriceDraw(ctx context.Context, rows []orderRemarkRow, dateFmt string) (int64, map[string]int64, error) {
|
||||
if len(rows) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// 1. 解析 remark,收集 unique activityIDs
|
||||
type parsed struct {
|
||||
activityID int64
|
||||
count int64
|
||||
dateKey string
|
||||
}
|
||||
var items []parsed
|
||||
idSet := make(map[int64]struct{})
|
||||
|
||||
for _, r := range rows {
|
||||
rmk := remark.Parse(r.Remark)
|
||||
if rmk.ActivityID > 0 {
|
||||
items = append(items, parsed{
|
||||
activityID: rmk.ActivityID,
|
||||
count: rmk.Count,
|
||||
dateKey: r.CreatedAt.Format(dateFmt),
|
||||
})
|
||||
idSet[rmk.ActivityID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 批量查 activities.price_draw(含软删除 Unscoped)
|
||||
actIDs := make([]int64, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
actIDs = append(actIDs, id)
|
||||
}
|
||||
priceMap := make(map[int64]int64)
|
||||
if len(actIDs) > 0 {
|
||||
var acts []model.Activities
|
||||
s.readDB.Activities.WithContext(ctx).UnderlyingDB().
|
||||
Unscoped().
|
||||
Table("activities").
|
||||
Select("id, price_draw").
|
||||
Where("id IN ?", actIDs).
|
||||
Find(&acts)
|
||||
for _, a := range acts {
|
||||
priceMap[a.ID] = a.PriceDraw
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 计算
|
||||
var total int64
|
||||
byDate := make(map[string]int64)
|
||||
for _, item := range items {
|
||||
if price, ok := priceMap[item.activityID]; ok {
|
||||
amt := price * item.count
|
||||
total += amt
|
||||
byDate[item.dateKey] += amt
|
||||
}
|
||||
}
|
||||
return total, byDate, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: 重写 `GetStats` — 日期逻辑修正 + 金额计算
|
||||
|
||||
**改动要点**:
|
||||
|
||||
1. **参数 `days` 真正按天**:`startDate = now.AddDate(0, 0, -days+1)`
|
||||
2. **Overview 全量不限时间**:用户数、订单数、实付金额均查全量
|
||||
3. **趋势按天分组**:`DATE_FORMAT(..., '%Y-%m-%d')` 替代 `'%Y-%m'`
|
||||
4. **金额用 price_draw**:调用 `calcPaidByPriceDraw`
|
||||
|
||||
```go
|
||||
func (s *service) GetStats(ctx context.Context, channelID int64, days int, startDateStr, endDateStr string) (*StatsOutput, error) {
|
||||
now := time.Now()
|
||||
|
||||
// 校验渠道存在
|
||||
_, err := s.readDB.Channels.WithContext(ctx).Where(s.readDB.Channels.ID.Eq(channelID)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrChannelNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &StatsOutput{}
|
||||
|
||||
// ========== 1. Overview(全量,不限时间)==========
|
||||
|
||||
// 1a. 累计用户
|
||||
userCount, _ := s.readDB.Users.WithContext(ctx).
|
||||
Where(s.readDB.Users.ChannelID.Eq(channelID)).Count()
|
||||
out.Overview.TotalUsers = userCount
|
||||
|
||||
// 1b. 累计订单数
|
||||
orderFilter := "users.channel_id = ? AND users.deleted_at IS NULL AND orders.status = 2 AND orders.actual_amount > 0 AND orders.source_type IN (1,2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)"
|
||||
|
||||
type countResult struct{ Count int64 }
|
||||
var cr countResult
|
||||
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||
Joins("JOIN users ON users.id = orders.user_id").
|
||||
Select("count(*) as count").
|
||||
Where(orderFilter, channelID).
|
||||
Scan(&cr)
|
||||
out.Overview.TotalOrders = cr.Count
|
||||
|
||||
// 1c. 累计实付金额(全量订单 remark → price_draw × count)
|
||||
var allRemarks []orderRemarkRow
|
||||
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||
Joins("JOIN users ON users.id = orders.user_id").
|
||||
Select("orders.remark, orders.created_at").
|
||||
Where(orderFilter, channelID).
|
||||
Scan(&allRemarks)
|
||||
|
||||
totalPaid, _, _ := s.calcPaidByPriceDraw(ctx, allRemarks, "2006-01-02")
|
||||
out.Overview.TotalPaidCents = totalPaid
|
||||
out.Overview.TotalGMV = totalPaid / 100
|
||||
|
||||
// ========== 2. 趋势图(按天分组,受 days 限制)==========
|
||||
|
||||
// 2a. 计算日期范围
|
||||
var startDate, endDate time.Time
|
||||
if startDateStr != "" && endDateStr != "" {
|
||||
startDate, _ = time.Parse("2006-01-02", startDateStr)
|
||||
endDate, _ = time.Parse("2006-01-02", endDateStr)
|
||||
endDate = endDate.Add(24*time.Hour - time.Second)
|
||||
} else {
|
||||
if days <= 0 {
|
||||
days = 12
|
||||
}
|
||||
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).
|
||||
AddDate(0, 0, -days+1)
|
||||
endDate = now
|
||||
}
|
||||
|
||||
// 2b. 初始化日期桶(每天一个)
|
||||
dateMap := make(map[string]*StatsDailyItem)
|
||||
var dateList []string
|
||||
for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) {
|
||||
key := d.Format("2006-01-02")
|
||||
dateList = append(dateList, key)
|
||||
dateMap[key] = &StatsDailyItem{Date: key}
|
||||
}
|
||||
|
||||
// 2c. 每日新增用户
|
||||
type dailyCount struct {
|
||||
Date string
|
||||
Count int64
|
||||
}
|
||||
var dailyUsers []dailyCount
|
||||
s.readDB.Users.WithContext(ctx).UnderlyingDB().Table("users").
|
||||
Select("DATE_FORMAT(created_at, '%Y-%m-%d') as date, count(*) as count").
|
||||
Where("channel_id = ? AND deleted_at IS NULL AND created_at >= ? AND created_at <= ?",
|
||||
channelID, startDate, endDate).
|
||||
Group("date").Scan(&dailyUsers)
|
||||
for _, u := range dailyUsers {
|
||||
if item, ok := dateMap[u.Date]; ok {
|
||||
item.UserCount = u.Count
|
||||
}
|
||||
}
|
||||
|
||||
// 2d. 每日订单数
|
||||
var dailyOrders []dailyCount
|
||||
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||
Joins("JOIN users ON users.id = orders.user_id").
|
||||
Select("DATE_FORMAT(orders.created_at, '%Y-%m-%d') as date, count(*) as count").
|
||||
Where(orderFilter+" AND orders.created_at >= ? AND orders.created_at <= ?",
|
||||
channelID, startDate, endDate).
|
||||
Group("date").Scan(&dailyOrders)
|
||||
for _, o := range dailyOrders {
|
||||
if item, ok := dateMap[o.Date]; ok {
|
||||
item.OrderCount = o.Count
|
||||
}
|
||||
}
|
||||
|
||||
// 2e. 每日实付金额(remark → price_draw)
|
||||
var rangeRemarks []orderRemarkRow
|
||||
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||
Joins("JOIN users ON users.id = orders.user_id").
|
||||
Select("orders.remark, orders.created_at").
|
||||
Where(orderFilter+" AND orders.created_at >= ? AND orders.created_at <= ?",
|
||||
channelID, startDate, endDate).
|
||||
Scan(&rangeRemarks)
|
||||
|
||||
_, dailyPaid, _ := s.calcPaidByPriceDraw(ctx, rangeRemarks, "2006-01-02")
|
||||
for dateKey, paid := range dailyPaid {
|
||||
if item, ok := dateMap[dateKey]; ok {
|
||||
item.PaidCents = paid
|
||||
item.GMV = paid / 100
|
||||
}
|
||||
}
|
||||
|
||||
// 2f. 组装输出
|
||||
for _, d := range dateList {
|
||||
out.Daily = append(out.Daily, *dateMap[d])
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: 同步修改 `List` 方法的金额计算
|
||||
|
||||
**文件**:`internal/service/channel/channel.go`,L206-223
|
||||
|
||||
**当前**:`SUM(orders.actual_amount)` 聚合。
|
||||
|
||||
**修改为**:按渠道查询所有订单 remark,分渠道调用 `calcPaidByPriceDraw`。
|
||||
|
||||
```go
|
||||
// 替换原有 paidResults 查询逻辑:
|
||||
if len(channelIDs) > 0 {
|
||||
// ... userCount 查询保持不变 ...
|
||||
|
||||
// 实付金额:查所有渠道的订单 remark
|
||||
type remarkWithChannel struct {
|
||||
ChannelID int64
|
||||
Remark string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
var chRemarks []remarkWithChannel
|
||||
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||
Joins("JOIN users ON users.id = orders.user_id").
|
||||
Select("users.channel_id, orders.remark, orders.created_at").
|
||||
Where("users.channel_id IN ? AND users.deleted_at IS NULL AND orders.status = 2 AND orders.actual_amount > 0 AND orders.source_type IN (1,2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)", channelIDs).
|
||||
Scan(&chRemarks)
|
||||
|
||||
// 按渠道分组
|
||||
grouped := make(map[int64][]orderRemarkRow)
|
||||
for _, r := range chRemarks {
|
||||
grouped[r.ChannelID] = append(grouped[r.ChannelID], orderRemarkRow{
|
||||
Remark: r.Remark, CreatedAt: r.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
for chID, rows := range grouped {
|
||||
total, _, _ := s.calcPaidByPriceDraw(ctx, rows, "2006-01-02")
|
||||
paidStats[chID] = total
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: 添加 remark import
|
||||
|
||||
确保文件顶部 import 包含:
|
||||
```go
|
||||
"bindbox-game/internal/pkg/util/remark"
|
||||
```
|
||||
|
||||
## 风险与缓解
|
||||
|
||||
| 风险 | 严重程度 | 缓解措施 |
|
||||
|------|---------|----------|
|
||||
| remark 格式不一致 | 中 | `remark.Parse()` 已处理 `activity:` 和 `lottery:activity:` 两种前缀 |
|
||||
| 软删除活动 | 已解决 | 使用 `Unscoped()` 查询,确保被删活动仍有 price_draw |
|
||||
| List 方法大量订单性能 | 中 | 单次查询所有渠道订单 remark,Go 中分组计算,比 N+1 高效 |
|
||||
| days 参数前端兼容 | 低 | 前端传 `days=12` 原意应为12天,修正后行为与参数名一致 |
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] Overview 累计用户:全量统计 `users.channel_id = X` 的用户数(不限时间)
|
||||
- [ ] Overview 累计订单:全量统计有效订单数(不限时间)
|
||||
- [ ] Overview 累计实付金额:全量基于 `activities.price_draw × count` 计算(包含软删除活动)
|
||||
- [ ] 趋势图按**天**分组,`days` 参数控制显示天数
|
||||
- [ ] 渠道列表页 `paid_amount` 同步使用 price_draw 计算
|
||||
- [ ] 编译通过 `make build-mac`
|
||||
- [ ] 现有功能无回归
|
||||
|
||||
## SESSION_ID(供 /ccg:execute 使用)
|
||||
|
||||
- CODEX_SESSION: N/A
|
||||
- GEMINI_SESSION: N/A
|
||||
@ -1,187 +0,0 @@
|
||||
# 渠道统计 — 盈亏计算
|
||||
|
||||
## 需求概述
|
||||
|
||||
在 `/admin/channels/:id/stats` 接口的 Overview 和趋势图中新增盈亏指标。
|
||||
|
||||
### 盈亏公式
|
||||
|
||||
```
|
||||
盈亏 = 收入(price_draw × count) - 成本(奖品价值 × 道具卡倍数)
|
||||
```
|
||||
|
||||
### 数据源
|
||||
|
||||
| 维度 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| **收入** | 已有 `calcPaidByPriceDraw` | 三路分类:抽奖/对对碰/一番赏 |
|
||||
| **成本** | `user_inventory.value_cents` | 奖品价值快照(分),fallback: `activity_reward_settings.price_snapshot_cents` → `products.price` |
|
||||
| **道具卡倍数** | `orders.item_card_id` → `user_item_cards.card_id` → `system_item_cards.reward_multiplier_x1000` | 双倍卡 = 2000(千分比),无卡 = 1000 |
|
||||
|
||||
### 成本计算公式(参考已有 dashboard_activity.go:L234-239)
|
||||
|
||||
```sql
|
||||
单件成本 = COALESCE(NULLIF(user_inventory.value_cents, 0),
|
||||
activity_reward_settings.price_snapshot_cents,
|
||||
products.price, 0)
|
||||
|
||||
道具卡倍数 = GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) / 1000
|
||||
|
||||
总成本 = SUM(单件成本 × 道具卡倍数)
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Step 1: 扩展响应结构体
|
||||
|
||||
**文件**:`internal/service/channel/channel.go`
|
||||
|
||||
```go
|
||||
type StatsOverview struct {
|
||||
TotalUsers int64 `json:"total_users"`
|
||||
TotalOrders int64 `json:"total_orders"`
|
||||
TotalGMV int64 `json:"total_gmv"`
|
||||
TotalPaidCents int64 `json:"total_paid_cents"`
|
||||
// 新增
|
||||
TotalCostCents int64 `json:"total_cost_cents"` // 总成本(分)
|
||||
TotalProfitCents int64 `json:"total_profit_cents"` // 盈亏(分) = paid - cost
|
||||
TotalCost int64 `json:"total_cost"` // 总成本(元)
|
||||
TotalProfit int64 `json:"total_profit"` // 盈亏(元)
|
||||
}
|
||||
|
||||
type StatsDailyItem struct {
|
||||
Date string `json:"date"`
|
||||
UserCount int64 `json:"user_count"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
GMV int64 `json:"gmv"`
|
||||
PaidCents int64 `json:"paid_cents"`
|
||||
// 新增
|
||||
CostCents int64 `json:"cost_cents"` // 当日成本(分)
|
||||
ProfitCents int64 `json:"profit_cents"` // 当日盈亏(分)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: 新增 `calcCostByInventory` 辅助函数
|
||||
|
||||
**文件**:`internal/service/channel/channel.go`
|
||||
|
||||
**输入**:渠道用户 ID 列表 + 日期范围(可选)
|
||||
**输出**:总成本(分)、按日期分组的成本
|
||||
|
||||
```go
|
||||
type costRow struct {
|
||||
ValueCents int64
|
||||
Multiplier int64 // reward_multiplier_x1000,无卡时=1000
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (s *service) calcCostByInventory(ctx context.Context, channelID int64, dateFmt string, startDate, endDate *time.Time) (int64, map[string]int64) {
|
||||
// SQL 核心逻辑(复用 dashboard_activity.go:L234-239 模式):
|
||||
//
|
||||
// SELECT
|
||||
// COALESCE(NULLIF(ui.value_cents, 0), ars.price_snapshot_cents, p.price, 0) AS unit_cost,
|
||||
// GREATEST(COALESCE(sic.reward_multiplier_x1000, 1000), 1000) AS multiplier,
|
||||
// ui.created_at
|
||||
// FROM user_inventory ui
|
||||
// JOIN users u ON u.id = ui.user_id
|
||||
// LEFT JOIN orders o ON o.id = ui.order_id
|
||||
// LEFT JOIN activity_reward_settings ars ON ars.id = ui.reward_id
|
||||
// LEFT JOIN products p ON p.id = ui.product_id
|
||||
// LEFT JOIN user_item_cards uic ON uic.id = o.item_card_id
|
||||
// LEFT JOIN system_item_cards sic ON sic.id = uic.card_id
|
||||
// WHERE u.channel_id = ? AND u.deleted_at IS NULL
|
||||
// AND ui.status IN (1, 3) -- 持有 or 已使用/发货
|
||||
// AND COALESCE(ui.remark, '') NOT LIKE '%void%'
|
||||
// AND (o.status = 2 OR ui.order_id = 0 OR ui.order_id IS NULL) -- 兼容历史
|
||||
// [AND ui.created_at >= ? AND ui.created_at <= ?] -- 可选时间范围
|
||||
|
||||
// Go 侧计算:
|
||||
// for each row:
|
||||
// cost += unit_cost * multiplier / 1000
|
||||
// byDate[dateKey] += unit_cost * multiplier / 1000
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 通过 `users.channel_id` 过滤渠道用户
|
||||
- `ui.status IN (1, 3)`:只统计有效资产(持有 + 已发货),排除作废
|
||||
- `NOT LIKE '%void%'`:排除作废备注
|
||||
- `(o.status = 2 OR ui.order_id = 0 OR ui.order_id IS NULL)`:兼容历史数据
|
||||
- 道具卡倍数通过 `orders.item_card_id` → `user_item_cards.card_id` → `system_item_cards.reward_multiplier_x1000` 链路获取
|
||||
|
||||
### Step 3: 在 `GetStats` 中调用成本计算
|
||||
|
||||
**文件**:`internal/service/channel/channel.go`,`GetStats` 方法
|
||||
|
||||
```go
|
||||
// ========== Overview 全量成本 ==========
|
||||
totalCost, _ := s.calcCostByInventory(ctx, channelID, "2006-01-02", nil, nil)
|
||||
out.Overview.TotalCostCents = totalCost
|
||||
out.Overview.TotalCost = totalCost / 100
|
||||
out.Overview.TotalProfitCents = out.Overview.TotalPaidCents - totalCost
|
||||
out.Overview.TotalProfit = out.Overview.TotalProfitCents / 100
|
||||
|
||||
// ========== 趋势图日维度成本 ==========
|
||||
_, dailyCost := s.calcCostByInventory(ctx, channelID, "2006-01-02", &startDate, &endDate)
|
||||
for dateKey, cost := range dailyCost {
|
||||
if item, ok := dateMap[dateKey]; ok {
|
||||
item.CostCents = cost
|
||||
item.ProfitCents = item.PaidCents - cost
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: 在 `List` 中可选加入成本(列表页)
|
||||
|
||||
**暂不实施**。列表页已有 `paid_amount`,盈亏是详情页指标,列表页展示所有渠道的成本查询开销较大。后续按需添加。
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `internal/service/channel/channel.go` | 修改 | 扩展结构体 + 新增 `calcCostByInventory` + 修改 `GetStats` |
|
||||
|
||||
## 查询关系链
|
||||
|
||||
```
|
||||
user_inventory
|
||||
├── JOIN users ON users.id = ui.user_id (过滤渠道)
|
||||
├── LEFT JOIN orders ON orders.id = ui.order_id (获取 item_card_id)
|
||||
├── LEFT JOIN activity_reward_settings ON ars.id = ui.reward_id (价格快照)
|
||||
├── LEFT JOIN products ON p.id = ui.product_id (商品价格 fallback)
|
||||
├── LEFT JOIN user_item_cards ON uic.id = o.item_card_id (道具卡实例)
|
||||
└── LEFT JOIN system_item_cards ON sic.id = uic.card_id (道具卡倍数)
|
||||
```
|
||||
|
||||
## 道具卡逻辑说明
|
||||
|
||||
| 场景 | `reward_multiplier_x1000` | 效果 | 成本影响 |
|
||||
|------|---------------------------|------|---------|
|
||||
| 无道具卡 | NULL → COALESCE → 1000 | ×1.0 | 成本 = 奖品原价 |
|
||||
| 双倍卡 | 2000 | ×2.0 | 成本 = 奖品原价 × 2 |
|
||||
| 三倍卡(如有) | 3000 | ×3.0 | 成本 = 奖品原价 × 3 |
|
||||
|
||||
**原理**:双倍卡让用户以相同支付价格获得双倍奖品,收入不变但成本翻倍,利润下降。
|
||||
|
||||
## 风险与缓解
|
||||
|
||||
| 风险 | 严重程度 | 缓解措施 |
|
||||
|------|---------|----------|
|
||||
| `user_inventory` 数据量大,全量查询慢 | 中 | 通过 `users.channel_id` 索引过滤,只查渠道用户 |
|
||||
| 历史资产无 `order_id` | 已解决 | `(o.status = 2 OR ui.order_id = 0 OR ui.order_id IS NULL)` 兼容 |
|
||||
| `value_cents = 0` 的历史数据 | 已解决 | COALESCE 链式 fallback 到 `price_snapshot_cents` → `products.price` |
|
||||
| 概率提升卡(EffectType=2)不影响成本 | 低 | `reward_multiplier_x1000` 只在 EffectType=1 时 > 1000,概率卡该字段为 1000,GREATEST 确保最小为 1.0 |
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] Overview 新增 `total_cost_cents`、`total_profit_cents`、`total_cost`、`total_profit`
|
||||
- [ ] 趋势图每天新增 `cost_cents`、`profit_cents`
|
||||
- [ ] 道具卡(双倍)正确计入成本(×2)
|
||||
- [ ] 无道具卡时成本不受影响(×1)
|
||||
- [ ] 成本计算排除 status=2(作废)和 void 备注的资产
|
||||
- [ ] 编译通过 `make build-mac`
|
||||
|
||||
## SESSION_ID(供 /ccg:execute 使用)
|
||||
|
||||
- CODEX_SESSION: N/A
|
||||
- GEMINI_SESSION: N/A
|
||||
@ -1,613 +0,0 @@
|
||||
# 渠道管理与用户注册绑定调用链文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述 Bindbox Game 项目中渠道管理模块与用户注册绑定的调用链关系。
|
||||
|
||||
**渠道绑定的三种方式:**
|
||||
1. **用户登录时绑定** - 微信/抖音登录时传入 `channel_code`
|
||||
2. **定时任务自动绑定** - 直播间奖品发放时,根据活动关联渠道自动绑定主播邀请人
|
||||
3. **抖音登录绑定** - 抖音小程序登录时传入 `channel_code`
|
||||
|
||||
---
|
||||
|
||||
## 一、数据模型
|
||||
|
||||
### 1.1 渠道表 (channels)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int64 | 主键ID |
|
||||
| name | string | 渠道名称 |
|
||||
| code | string | 渠道唯一标识(用于登录时绑定) |
|
||||
| type | string | 渠道类型 |
|
||||
| remarks | string | 备注 |
|
||||
| created_at | time | 创建时间 |
|
||||
| updated_at | time | 更新时间 |
|
||||
| deleted_at | time | 删除时间(软删) |
|
||||
|
||||
**文件位置**: `internal/repository/mysql/model/channels.gen.go:16-25`
|
||||
|
||||
### 1.2 用户表 (users) - 渠道相关字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int64 | 主键ID |
|
||||
| channel_id | int64 | 渠道ID(关联 channels.id) |
|
||||
| invite_code | string | 用户唯一邀请码 |
|
||||
| inviter_id | int64 | 邀请人用户ID |
|
||||
| openid | string | 微信openid |
|
||||
| unionid | string | 微信unionid |
|
||||
|
||||
**文件位置**: `internal/repository/mysql/model/users.gen.go:16-33`
|
||||
|
||||
---
|
||||
|
||||
## 二、调用链架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 前端/客户端 │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ API 路由层 │
|
||||
│ internal/router/router.go │
|
||||
│ │
|
||||
│ 渠道管理路由: │
|
||||
│ - POST /api/admin/channels → CreateChannel() │
|
||||
│ - PUT /api/admin/channels/:id → ModifyChannel() │
|
||||
│ - DELETE /api/admin/channels/:id → DeleteChannel() │
|
||||
│ - GET /api/admin/channels → ListChannels() │
|
||||
│ - GET /api/admin/channels/:id/stats → ChannelStats() │
|
||||
│ │
|
||||
│ 用户登录路由: │
|
||||
│ - POST /api/app/users/weixin/login → WeixinLogin() │
|
||||
│ (携带 channel_code 参数) │
|
||||
│ - POST /api/app/users/douyin/login → DouyinLogin() │
|
||||
│ (携带 channel_code 参数) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
|
||||
│ 渠道管理 API 层 │ │ 用户登录 API 层 │ │ 定时任务调度器 │
|
||||
│ internal/api/admin/ │ │ internal/api/user/│ │ internal/service/ │
|
||||
│ channels.go │ │ login_app.go │ │ douyin/scheduler.go │
|
||||
│ │ │ login_douyin_app │ │ │
|
||||
│ - CreateChannel() │ │ - WeixinLogin() │ │ - GrantLivestreamPrizes()
|
||||
│ - ModifyChannel() │ │ - DouyinLogin() │ │ - bindAnchorInviter │
|
||||
│ - DeleteChannel() │ │ 接收channel_code│ │ IfNeeded() │
|
||||
│ - ListChannels() │ │ │ │ │
|
||||
│ - ChannelStats() │ │ │ │ 每5分钟自动执行 │
|
||||
└─────────────────────────┘ └───────────────────┘ └───────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
|
||||
│ 渠道 Service 层 │ │ 用户 Service 层 │ │ 用户 Service 层 │
|
||||
│ internal/service/ │ │ internal/service/ │ │ internal/service/user │
|
||||
│ channel/channel.go │ │ user/ │ │ │
|
||||
│ │ │ login_weixin.go │ │ - BindInviter() │
|
||||
│ - Create() │ │ login_douyin.go │ │ (定时任务调用) │
|
||||
│ - Modify() │ │ │ │ │
|
||||
│ - Delete() │ │ - LoginWeixin() │ │ │
|
||||
│ - List() │ │ - LoginDouyin() │ │ │
|
||||
│ - GetStats() │ │ 查渠道并绑定用户 │ │ │
|
||||
│ - GetByID() │ │ │ │ │
|
||||
└─────────────────────────┘ └───────────────────┘ └───────────────────────┘
|
||||
│ │ │
|
||||
└─────────────────────┼─────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 数据访问层 (DAO) │
|
||||
│ internal/repository/mysql/dao/ │
|
||||
│ │
|
||||
│ - channels.gen.go 渠道表操作 │
|
||||
│ - users.gen.go 用户表操作 │
|
||||
│ - user_invites.gen.go 邀请关系表操作 │
|
||||
│ - livestream_activities.gen.go 直播间活动表(含渠道字段) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ MySQL 数据库 │
|
||||
│ - channels 表 │
|
||||
│ - users 表 (channel_id 字段) │
|
||||
│ - user_invites 表 │
|
||||
│ - livestream_activities 表 (channel_id, channel_code 字段) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、详细调用链
|
||||
|
||||
### 3.1 渠道管理(管理端)
|
||||
|
||||
#### 创建渠道
|
||||
|
||||
```
|
||||
HTTP POST /api/admin/channels
|
||||
│
|
||||
├── 参数: { name, code, type, remarks }
|
||||
│
|
||||
▼
|
||||
internal/api/admin/channels.go:26 CreateChannel()
|
||||
│
|
||||
├── 验证参数
|
||||
│
|
||||
▼
|
||||
internal/service/channel/channel.go:79 Create()
|
||||
│
|
||||
├── 创建 Channels 模型
|
||||
│
|
||||
▼
|
||||
dao.Channels.Create(m)
|
||||
│
|
||||
▼
|
||||
MySQL INSERT INTO channels
|
||||
```
|
||||
|
||||
#### 查询渠道列表(含用户数统计)
|
||||
|
||||
```
|
||||
HTTP GET /api/admin/channels
|
||||
│
|
||||
▼
|
||||
internal/api/admin/channels.go:135 ListChannels()
|
||||
│
|
||||
▼
|
||||
internal/service/channel/channel.go:111 List()
|
||||
│
|
||||
├── 1. 查询渠道列表
|
||||
│ SELECT * FROM channels WHERE name LIKE ? ORDER BY id DESC
|
||||
│
|
||||
├── 2. 统计每个渠道的用户数
|
||||
│ SELECT channel_id, count(*) as count
|
||||
│ FROM users
|
||||
│ WHERE channel_id IN (?)
|
||||
│ GROUP BY channel_id
|
||||
│
|
||||
▼
|
||||
返回渠道列表(含 user_count 字段)
|
||||
```
|
||||
|
||||
#### 渠道数据分析
|
||||
|
||||
```
|
||||
HTTP GET /api/admin/channels/:channel_id/stats
|
||||
│
|
||||
▼
|
||||
internal/api/admin/channels.go:53 ChannelStats()
|
||||
│
|
||||
▼
|
||||
internal/service/channel/channel.go:169 GetStats()
|
||||
│
|
||||
├── 1. 统计渠道用户总数
|
||||
│ SELECT count(*) FROM users WHERE channel_id = ?
|
||||
│
|
||||
├── 2. 统计渠道订单数和GMV
|
||||
│ SELECT count(*) as count, sum(actual_amount) as gmv
|
||||
│ FROM orders o
|
||||
│ JOIN users u ON u.id = o.user_id
|
||||
│ WHERE u.channel_id = ? AND o.status = 2
|
||||
│
|
||||
├── 3. 月度用户增长统计
|
||||
│ SELECT DATE_FORMAT(created_at, '%Y-%m') as date, count(*) as count
|
||||
│ FROM users
|
||||
│ WHERE channel_id = ? AND created_at >= ?
|
||||
│ GROUP BY date
|
||||
│
|
||||
├── 4. 月度订单统计
|
||||
│ SELECT DATE_FORMAT(created_at, '%Y-%m') as date, count(*), sum(actual_amount)
|
||||
│ FROM orders o
|
||||
│ JOIN users u ON u.id = o.user_id
|
||||
│ WHERE u.channel_id = ? AND o.status = 2 AND o.created_at >= ?
|
||||
│ GROUP BY date
|
||||
│
|
||||
▼
|
||||
返回渠道统计数据
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 用户注册绑定渠道
|
||||
|
||||
#### 微信登录(绑定渠道)
|
||||
|
||||
```
|
||||
HTTP POST /api/app/users/weixin/login
|
||||
│
|
||||
├── 参数: { code, invite_code, douyin_id, channel_code }
|
||||
│
|
||||
▼
|
||||
internal/api/user/login_app.go:47 WeixinLogin()
|
||||
│
|
||||
├── 1. 微信 code2session 获取 openid/unionid
|
||||
│
|
||||
▼
|
||||
internal/service/user/login_weixin.go:42 LoginWeixin()
|
||||
│
|
||||
├── 2. 查询渠道(如果传入 channel_code)
|
||||
│ ch, _ := s.readDB.Channels.Where(Channels.Code.Eq(in.ChannelCode)).First()
|
||||
│ channelID = ch.ID
|
||||
│ 【文件位置: login_weixin.go:86-92】
|
||||
│
|
||||
├── 3. 查找或创建用户
|
||||
│ ├── 查找: WHERE openid = ? OR unionid = ?
|
||||
│ │
|
||||
│ └── 创建新用户:
|
||||
│ u = &model.Users{
|
||||
│ Nickname: nickname,
|
||||
│ Openid: in.OpenID,
|
||||
│ ChannelID: channelID, // 绑定渠道
|
||||
│ ...
|
||||
│ }
|
||||
│ 【文件位置: login_weixin.go:113-124】
|
||||
│
|
||||
├── 4. 更新已有用户(如果传入 channel_code)
|
||||
│ if channelID > 0 {
|
||||
│ UPDATE users SET channel_id = ? WHERE id = ?
|
||||
│ }
|
||||
│ 【文件位置: login_weixin.go:141-143】
|
||||
│
|
||||
├── 5. 处理邀请关系(如果传入 invite_code 且是新用户)
|
||||
│ 【详见 3.3 节】
|
||||
│
|
||||
▼
|
||||
返回用户信息和 Token
|
||||
```
|
||||
|
||||
**关键代码片段**(`login_weixin.go:86-92`):
|
||||
|
||||
```go
|
||||
// 查找渠道ID
|
||||
var channelID int64
|
||||
if in.ChannelCode != "" {
|
||||
ch, _ := s.readDB.Channels.WithContext(ctx).Where(s.readDB.Channels.Code.Eq(in.ChannelCode)).First()
|
||||
if ch != nil {
|
||||
channelID = ch.ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 抖音登录(绑定渠道)
|
||||
|
||||
```
|
||||
HTTP POST /api/app/users/douyin/login
|
||||
│
|
||||
├── 参数: { code, anonymous_code, invite_code, channel_code }
|
||||
│
|
||||
▼
|
||||
internal/api/user/login_douyin_app.go:44 DouyinLogin()
|
||||
│
|
||||
├── 参数校验
|
||||
│
|
||||
▼
|
||||
internal/service/user/login_douyin.go:39 LoginDouyin()
|
||||
│
|
||||
├── 1. 抖音 code2session 获取 openid
|
||||
│
|
||||
├── 2. 查询渠道(如果传入 channel_code)
|
||||
│ ch, _ := s.readDB.Channels.Where(Channels.Code.Eq(in.ChannelCode)).First()
|
||||
│ channelID = ch.ID
|
||||
│ 【文件位置: login_douyin.go:91-97】
|
||||
│
|
||||
├── 3. 查找或创建用户
|
||||
│ ├── 查找: WHERE douyin_id = ? OR unionid = ?
|
||||
│ │
|
||||
│ └── 创建新用户:
|
||||
│ u = &model.Users{
|
||||
│ Nickname: nickname,
|
||||
│ DouyinID: openID,
|
||||
│ ChannelID: channelID, // 绑定渠道
|
||||
│ ...
|
||||
│ }
|
||||
│ 【文件位置: login_douyin.go:119-127】
|
||||
│
|
||||
├── 4. 更新已有用户(如果传入 channel_code 且未绑定)
|
||||
│ if channelID > 0 && u.ChannelID == 0 {
|
||||
│ UPDATE users SET channel_id = ?
|
||||
│ }
|
||||
│ 【文件位置: login_douyin.go:143-144】
|
||||
│
|
||||
└── 5. 处理邀请关系(如果传入 invite_code 且是新用户)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 邀请关系绑定
|
||||
|
||||
用户绑定邀请人有两种方式:
|
||||
|
||||
#### 方式一:登录时自动绑定(推荐)
|
||||
|
||||
```
|
||||
用户登录时传入 invite_code 参数
|
||||
│
|
||||
▼
|
||||
LoginWeixin() / LoginDouyin() 内部处理
|
||||
│
|
||||
├── 检查是否新用户
|
||||
│
|
||||
├── 查找邀请人(通过 invite_code)
|
||||
│
|
||||
├── 创建 user_invites 记录
|
||||
│
|
||||
├── 更新 users.inviter_id
|
||||
│
|
||||
▼
|
||||
触发任务中心奖励: task.OnInviteSuccess()
|
||||
```
|
||||
|
||||
#### 方式二:用户主动绑定
|
||||
|
||||
```
|
||||
HTTP POST /api/app/users/inviter/bind
|
||||
│
|
||||
├── 参数: { invite_code }
|
||||
│
|
||||
▼
|
||||
internal/api/user/bind_inviter_app.go:34 BindInviter()
|
||||
│
|
||||
▼
|
||||
internal/service/user/bind_inviter.go:33 BindInviter()
|
||||
│
|
||||
├── 1. 加锁获取当前用户
|
||||
│
|
||||
├── 2. 检查是否已绑定(inviter_id != 0 则拒绝)
|
||||
│
|
||||
├── 3. 查找邀请人
|
||||
│
|
||||
├── 4. 创建 user_invites 记录
|
||||
│
|
||||
├── 5. 更新 users.inviter_id
|
||||
│
|
||||
▼
|
||||
触发任务中心奖励: task.OnInviteSuccess()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 定时任务自动绑定渠道(直播间奖品发放)
|
||||
|
||||
**重要场景:直播间用户通过渠道绑定主播邀请人**
|
||||
|
||||
```
|
||||
定时任务 (每5分钟)
|
||||
│
|
||||
▼
|
||||
internal/service/douyin/scheduler.go:24 StartDouyinOrderSync()
|
||||
│
|
||||
├── ticker5min.C 触发
|
||||
│
|
||||
▼
|
||||
internal/service/douyin/scheduler.go:155 GrantLivestreamPrizes()
|
||||
│
|
||||
├── 1. 查找未发放的直播抽奖记录
|
||||
│ SELECT * FROM livestream_draw_logs WHERE is_granted = 0
|
||||
│
|
||||
├── 2. 解析活动关联的渠道/主播邀请码
|
||||
│ resolveActivityAnchorCodes()
|
||||
│ 【文件位置: scheduler.go:418-489】
|
||||
│
|
||||
│ ├── 查询直播间活动的渠道信息
|
||||
│ │ SELECT id, channel_id, channel_code
|
||||
│ │ FROM livestream_activities
|
||||
│ │ WHERE id IN (?)
|
||||
│ │ 【文件位置: scheduler.go:451-458】
|
||||
│ │
|
||||
│ └── 补充缺失的渠道 code
|
||||
│ fetchChannelCodes()
|
||||
│ SELECT id, code FROM channels WHERE id IN (?)
|
||||
│ 【文件位置: scheduler.go:491-513】
|
||||
│
|
||||
├── 3. 自动绑定主播邀请人(如果用户未绑定)
|
||||
│ bindAnchorInviterIfNeeded(ctx, userID, anchorCode)
|
||||
│ 【文件位置: scheduler.go:515-546】
|
||||
│
|
||||
│ ├── 查询用户是否已有邀请人
|
||||
│ │ SELECT inviter_id FROM users WHERE id = ?
|
||||
│ │
|
||||
│ └── 如果 inviter_id == 0,调用绑定服务
|
||||
│ s.userSvc.BindInviter(ctx, userID, BindInviterInput{InviteCode: anchorCode})
|
||||
│ 【文件位置: scheduler.go:534】
|
||||
│
|
||||
└── 4. 发放奖品并更新状态
|
||||
```
|
||||
|
||||
**关键代码:自动绑定主播邀请人**
|
||||
|
||||
```go
|
||||
// scheduler.go:515-546
|
||||
func (s *service) bindAnchorInviterIfNeeded(ctx context.Context, userID int64, anchorCode string) {
|
||||
// 1. 检查用户是否已有邀请人
|
||||
userRecord, err := s.readDB.Users.WithContext(ctx).
|
||||
Select(s.readDB.Users.InviterID).
|
||||
Where(s.readDB.Users.ID.Eq(userID)).
|
||||
First()
|
||||
if userRecord.InviterID != 0 {
|
||||
return // 已绑定,跳过
|
||||
}
|
||||
|
||||
// 2. 自动绑定主播邀请人
|
||||
s.userSvc.BindInviter(ctx, userID, user.BindInviterInput{InviteCode: anchorCode})
|
||||
}
|
||||
```
|
||||
|
||||
**数据流:**
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 直播间活动配置 │
|
||||
│ livestream_activities │
|
||||
│ ├── channel_id (关联渠道ID) │
|
||||
│ └── channel_code (主播邀请码) │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 直播抽奖记录 │
|
||||
│ livestream_draw_logs │
|
||||
│ ├── activity_id (关联活动) │
|
||||
│ ├── local_user_id (本地用户ID) │
|
||||
│ └── is_granted (发放状态) │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 定时任务处理 │
|
||||
│ GrantLivestreamPrizes() │
|
||||
│ │
|
||||
│ 1. 查 activity → 获取 channel_code │
|
||||
│ 2. 查 channels → 补充缺失的 code │
|
||||
│ 3. 查 users.inviter_id → 检查是否已绑定 │
|
||||
│ 4. 未绑定 → 调用 BindInviter() 绑定主播 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 用户邀请关系 │
|
||||
│ users.inviter_id = 主播用户ID │
|
||||
│ user_invites 表新增记录 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、核心文件索引
|
||||
|
||||
| 文件路径 | 说明 | 关键函数 |
|
||||
|----------|------|----------|
|
||||
| `internal/api/admin/channels.go` | 渠道管理 API | CreateChannel, ListChannels, ChannelStats |
|
||||
| `internal/service/channel/channel.go` | 渠道业务逻辑 | Create, List, GetStats |
|
||||
| `internal/api/user/login_app.go` | 用户登录 API (微信) | WeixinLogin |
|
||||
| `internal/service/user/login_weixin.go` | 微信登录逻辑 | LoginWeixin(渠道绑定核心) |
|
||||
| `internal/api/user/login_douyin_app.go` | 用户登录 API (抖音) | DouyinLogin |
|
||||
| `internal/service/user/login_douyin.go` | 抖音登录逻辑 | LoginDouyin(渠道绑定核心) |
|
||||
| `internal/api/user/bind_inviter_app.go` | 绑定邀请人 API | BindInviter |
|
||||
| `internal/service/user/bind_inviter.go` | 绑定邀请人逻辑 | BindInviter |
|
||||
| **`internal/service/douyin/scheduler.go`** | **抖音定时任务** | **GrantLivestreamPrizes, bindAnchorInviterIfNeeded** |
|
||||
| `internal/repository/mysql/model/channels.gen.go` | 渠道模型 | Channels struct |
|
||||
| `internal/repository/mysql/model/users.gen.go` | 用户模型 | Users struct(含 channel_id) |
|
||||
| `internal/repository/mysql/model/user_invites.gen.go` | 邀请关系模型 | UserInvites struct |
|
||||
| `internal/repository/mysql/model/livestream_activities.gen.go` | 直播间活动模型 | LivestreamActivities(含 channel_id, channel_code) |
|
||||
| `internal/router/router.go` | 路由配置 | 渠道路由: 215-219 行 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据流图
|
||||
|
||||
### 5.1 用户注册绑定渠道流程
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌────────────────┐
|
||||
│ 前端 │────▶│ 微信登录 API │────▶│ 用户 Service │
|
||||
│ │ │ │ │ │
|
||||
│ channel_ │ │ code │ │ 1. code2session│
|
||||
│ code │ │ invite_code │ │ 2. 查渠道 │
|
||||
└──────────┘ └──────────────┘ │ 3. 创建/更新用户│
|
||||
│ 4. 绑定邀请人 │
|
||||
└───────┬────────┘
|
||||
│
|
||||
┌─────────────────────────┼─────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ channels 表 │ │ users 表 │ │user_invites表│
|
||||
│ │ │ │ │ │
|
||||
│ code → ID │ │ channel_id │ │ inviter_id │
|
||||
│ │ │ inviter_id │ │ invitee_id │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### 5.2 渠道统计查询流程
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌────────────────┐
|
||||
│ 管理后台 │────▶│ 渠道统计 API │────▶│ 渠道 Service │
|
||||
│ │ │ │ │ │
|
||||
│ 选择渠道 │ │ channel_id │ │ 1. 统计用户数 │
|
||||
│ 时间范围 │ │ days │ │ 2. 统计订单数 │
|
||||
└──────────┘ │ start_date │ │ 3. 统计GMV │
|
||||
│ end_date │ │ 4. 月度趋势 │
|
||||
└──────────────┘ └───────┬────────┘
|
||||
│
|
||||
┌─────────────────────────┴────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ users 表 │ │ orders 表 │
|
||||
│ │ │ │
|
||||
│ WHERE │ │ JOIN users │
|
||||
│ channel_id=? │ │ WHERE │
|
||||
│ │ │ channel_id=? │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、业务规则
|
||||
|
||||
### 6.1 渠道绑定规则
|
||||
|
||||
| 场景 | 触发条件 | 说明 |
|
||||
|------|----------|------|
|
||||
| 微信登录绑定 | 传入 `channel_code` | 查询 channels 表,绑定 channel_id 到用户 |
|
||||
| 抖音登录绑定 | 传入 `channel_code` | 查询 channels 表,仅当用户未绑定时才更新 |
|
||||
| 定时任务绑定 | 直播间活动配置了 `channel_code` | 自动绑定主播邀请人(邀请关系,非渠道) |
|
||||
|
||||
### 6.2 渠道与邀请人的区别
|
||||
|
||||
| 概念 | 字段 | 说明 |
|
||||
|------|------|------|
|
||||
| **渠道 (Channel)** | `users.channel_id` | 用户来源渠道,用于统计分析 |
|
||||
| **邀请人 (Inviter)** | `users.inviter_id` | 邀请该用户注册的人,用于奖励计算 |
|
||||
|
||||
**定时任务场景:**
|
||||
- 直播间活动的 `channel_code` 用作**主播邀请码**
|
||||
- 定时任务调用 `BindInviter()` 绑定的是**邀请关系**,而非渠道
|
||||
- 主播邀请码 = 某个用户的 `invite_code`(通常是主播账号)
|
||||
|
||||
### 6.3 邀请绑定规则
|
||||
|
||||
1. **仅限一次**: 用户绑定邀请人后不可更改
|
||||
2. **不能自邀**: 用户不能邀请自己
|
||||
3. **奖励触发**: 绑定成功后触发任务中心奖励逻辑
|
||||
4. **定时任务补绑**: 直播间用户未绑定邀请人时,自动绑定主播
|
||||
|
||||
### 6.4 权限控制
|
||||
|
||||
渠道管理接口需要以下权限:
|
||||
|
||||
| 操作 | 权限标识 |
|
||||
|------|----------|
|
||||
| 创建渠道 | `channel:create` |
|
||||
| 修改渠道 | `channel:modify` |
|
||||
| 删除渠道 | `channel:delete` |
|
||||
| 查看渠道 | `channel:view` |
|
||||
|
||||
---
|
||||
|
||||
## 七、相关迁移文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `migrations/20260223_add_channel_fields_to_livestream_activities.sql` | 直播间活动添加渠道字段 |
|
||||
|
||||
---
|
||||
|
||||
## 八、注意事项
|
||||
|
||||
1. **渠道 Code 唯一性**: 渠道的 `code` 字段必须唯一,用于用户登录时匹配
|
||||
2. **统计性能**: 渠道统计涉及多表 JOIN,大数据量时需注意性能优化
|
||||
3. **事务处理**: 用户创建和渠道绑定在同一事务中,保证数据一致性
|
||||
4. **软删除**: 渠道删除为软删除,不影响已绑定用户
|
||||
|
||||
---
|
||||
|
||||
*文档生成时间: 2026-02-27*
|
||||
*项目: bindbox_game*
|
||||
@ -1,135 +0,0 @@
|
||||
## 实施计划:GMV 支付方式拆分展示
|
||||
|
||||
### 需求分析
|
||||
|
||||
当前渠道统计只展示一个"累计实付金额"(GMV 总数),用户无法看到这笔钱的构成。需要拆分为:
|
||||
- **现金支付** (`actual_amount`) — 用户通过微信支付的真金白银
|
||||
- **优惠券抵扣** (`discount_amount`) — 优惠券抵扣部分
|
||||
- **积分抵扣** (`points_amount`) — 积分抵扣部分(当前数据为0,但字段已预留)
|
||||
|
||||
验证:`total_amount = actual_amount + discount_amount + points_amount` 在所有订单上完全成立(0条不等式)。
|
||||
|
||||
### 数据现状(dev 环境)
|
||||
|
||||
| 支付方式 | 订单数 | 金额(元) | 占比 |
|
||||
|---------|--------|---------|------|
|
||||
| GMV 总额 | 3,595 | 124,526.80 | 100% |
|
||||
| 现金 | 3,595 | 90,067.85 | 72.3% |
|
||||
| 优惠券 | 1,896 | 34,458.95 | 27.7% |
|
||||
| 积分 | 0 | 0.00 | 0% |
|
||||
|
||||
### 技术方案
|
||||
|
||||
orders 表已有完整的拆分字段,**无需新建表或字段**,只需在查询和展示层增加维度。
|
||||
|
||||
### 实施步骤
|
||||
|
||||
#### Step 1: 后端 — 扩展数据结构
|
||||
|
||||
文件:`internal/service/channel/channel.go`
|
||||
|
||||
1.1 `StatsOverview` 结构体新增字段:
|
||||
```go
|
||||
CashCents int64 `json:"cash_cents"` // 现金支付(分)
|
||||
CouponCents int64 `json:"coupon_cents"` // 优惠券抵扣(分)
|
||||
PointsCents int64 `json:"points_cents"` // 积分抵扣(分)
|
||||
```
|
||||
|
||||
1.2 `StatsDailyItem` 结构体新增字段:
|
||||
```go
|
||||
CashCents int64 `json:"cash_cents"`
|
||||
CouponCents int64 `json:"coupon_cents"`
|
||||
PointsCents int64 `json:"points_cents"`
|
||||
```
|
||||
|
||||
#### Step 2: 后端 — 修改 GMV 查询方法
|
||||
|
||||
文件:`internal/service/channel/channel.go`
|
||||
|
||||
2.1 `calcGMVByTotalAmount` 改为同时返回 actual_amount / discount_amount / points_amount 的分组统计:
|
||||
|
||||
```go
|
||||
type GMVBreakdown struct {
|
||||
Total int64
|
||||
Cash int64
|
||||
Coupon int64
|
||||
Points int64
|
||||
}
|
||||
|
||||
func calcGMVByTotalAmount(...) (GMVBreakdown, map[string]GMVBreakdown)
|
||||
```
|
||||
|
||||
查询 SELECT 增加:`orders.actual_amount, orders.discount_amount, orders.points_amount`
|
||||
|
||||
2.2 `GetStats` 方法中将拆分数据写入 Overview 和 Daily:
|
||||
```go
|
||||
out.Overview.CashCents = breakdown.Cash
|
||||
out.Overview.CouponCents = breakdown.Coupon
|
||||
out.Overview.PointsCents = breakdown.Points
|
||||
```
|
||||
|
||||
#### Step 3: 后端 — List 接口也返回拆分数据(可选)
|
||||
|
||||
文件:`internal/service/channel/channel.go`
|
||||
|
||||
`ChannelWithStat` 结构体和 `List` 方法中的 GMV 查询也增加拆分统计,用于渠道列表页 tooltip 展示。
|
||||
|
||||
#### Step 4: 前端 — API 类型更新
|
||||
|
||||
文件:`web/admin/src/api/channels.ts`
|
||||
|
||||
```ts
|
||||
interface StatsOverview {
|
||||
// ... existing fields
|
||||
cash_cents?: number
|
||||
coupon_cents?: number
|
||||
points_cents?: number
|
||||
}
|
||||
|
||||
interface StatsDailyItem {
|
||||
// ... existing fields
|
||||
cash_cents?: number
|
||||
coupon_cents?: number
|
||||
points_cents?: number
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 5: 前端 — Stats 概览卡片展示
|
||||
|
||||
文件:`web/admin/src/views/operations/channels/index.vue`
|
||||
|
||||
在"累计实付金额"卡片下方或旁边展示支付构成:
|
||||
- 显示 3 个子指标:现金 / 优惠券 / 积分
|
||||
- 各自显示金额和占比百分比
|
||||
- 积分为 0 时可隐藏或灰显
|
||||
|
||||
#### Step 6: 前端 — 每日趋势图支持
|
||||
|
||||
文件:`web/admin/src/views/operations/channels/index.vue`
|
||||
|
||||
在 revenue tab 的折线图中,可以选择查看:
|
||||
- GMV 总额(默认)
|
||||
- 现金 / 优惠券 / 积分 分层堆叠
|
||||
|
||||
#### Step 7: 测试
|
||||
|
||||
文件:`internal/service/channel/channel_stats_test.go`
|
||||
|
||||
更新测试用例,验证 GMV 拆分字段的正确性。
|
||||
|
||||
### 关键文件
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `internal/service/channel/channel.go` | 修改 | 数据结构 + 查询逻辑 |
|
||||
| `internal/service/channel/channel_stats_test.go` | 修改 | 测试覆盖拆分字段 |
|
||||
| `web/admin/src/api/channels.ts` | 修改 | API 类型定义 |
|
||||
| `web/admin/src/views/operations/channels/index.vue` | 修改 | 概览卡片 + 图表展示 |
|
||||
|
||||
### 风险与缓解
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| 旧版前端未适配新字段 | 新字段均为可选,不影响旧版展示 |
|
||||
| 积分字段当前全为0 | 字段预留,后续开启积分抵扣时自动生效 |
|
||||
| 查询性能 | 无额外 JOIN,只增加 3 个 SUM 列,影响可忽略 |
|
||||
@ -1,404 +0,0 @@
|
||||
# 📋 实施计划:扫雷排行榜管理后台 + 去除免费模式
|
||||
|
||||
## 背景理解
|
||||
|
||||
用户说明:
|
||||
1. **没有免费模式**:`minesweeper_free` 这个 game_type 已废弃,前后端都要移除
|
||||
2. **排行榜需要在管理后台展示**:当前排行榜只有 App 端接口,管理后台缺少排行榜 Tab
|
||||
3. **"积分"含义模糊**:排行榜里的 `total_rank_points` 字段是"游戏对战分",不是平台积分(points),页面上要加说明
|
||||
|
||||
---
|
||||
|
||||
## 任务类型
|
||||
- [x] 全栈(后端 + 前端并行)
|
||||
|
||||
---
|
||||
|
||||
## 技术方案
|
||||
|
||||
### 后端
|
||||
在 `internal/api/game/handler.go` 新增一个 Admin 专用排行榜接口:
|
||||
- `GET /api/admin/games/leaderboard` — 管理后台查排行榜(分页、支持搜索用户昵称)
|
||||
- `GET /api/admin/games/records` — 管理后台查每局对战记录(分页、支持按用户/时间筛选)
|
||||
|
||||
两个接口都走读库,无需鉴权以外的特殊处理。
|
||||
|
||||
### 前端
|
||||
在 `web/admin/src/views/operations/minesweeper/index.vue` 新增两个 Tab:
|
||||
- **排行榜 Tab**:表格展示所有玩家的排行数据,含"对战分"说明
|
||||
- **对战记录 Tab**:按局查每场游戏的明细
|
||||
|
||||
同时去掉前端中所有 `minesweeper_free` 的相关逻辑和 `game_type` 切换选项。
|
||||
|
||||
---
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Step 1 — 后端:新增 Admin 排行榜接口
|
||||
|
||||
**文件**: `internal/api/game/handler.go`(在现有 Admin API 区域末尾追加)
|
||||
|
||||
```go
|
||||
// GetAdminLeaderboard Admin查询扫雷排行榜
|
||||
// @Router /api/admin/games/leaderboard [get]
|
||||
func (h *handler) GetAdminLeaderboard() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
var req struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
Nickname string `form:"nickname"` // 可选:按昵称模糊搜索
|
||||
}
|
||||
_ = ctx.ShouldBindQuery(&req)
|
||||
if req.Page <= 0 { req.Page = 1 }
|
||||
if req.PageSize <= 0 || req.PageSize > 100 { req.PageSize = 20 }
|
||||
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
|
||||
type row struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
TotalRankPoints int64 `json:"total_rank_points"`
|
||||
MatchesPlayed int `json:"matches_played"`
|
||||
Wins int `json:"wins"`
|
||||
Losses int `json:"losses"`
|
||||
WinRate float64 `json:"win_rate"`
|
||||
BestScore int `json:"best_score"`
|
||||
AvgScore float64 `json:"avg_score"`
|
||||
}
|
||||
|
||||
query := h.db.GetDbR().Table("minesweeper_leaderboard l").
|
||||
Select("l.user_id, COALESCE(u.nick_name,'') AS nickname, COALESCE(u.avatar_url,'') AS avatar, l.total_rank_points, l.matches_played, l.wins, l.losses, CAST(l.win_rate AS DECIMAL(7,4)) AS win_rate, l.best_score, CAST(l.avg_score AS DECIMAL(12,2)) AS avg_score").
|
||||
Joins("LEFT JOIN users u ON u.id = l.user_id").
|
||||
Where("l.game_type = ?", "minesweeper")
|
||||
|
||||
if req.Nickname != "" {
|
||||
query = query.Where("u.nick_name LIKE ?", "%"+req.Nickname+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var rows []row
|
||||
query.Order("l.total_rank_points DESC, l.wins DESC, l.best_score DESC").
|
||||
Limit(req.PageSize).Offset(offset).Scan(&rows)
|
||||
|
||||
// 补名次
|
||||
list := make([]map[string]any, 0, len(rows))
|
||||
for i, r := range rows {
|
||||
list = append(list, map[string]any{
|
||||
"rank": offset + i + 1,
|
||||
"user_id": r.UserID,
|
||||
"nickname": r.Nickname,
|
||||
"avatar": r.Avatar,
|
||||
"total_rank_points": r.TotalRankPoints,
|
||||
"matches_played": r.MatchesPlayed,
|
||||
"wins": r.Wins,
|
||||
"losses": r.Losses,
|
||||
"win_rate": r.WinRate,
|
||||
"best_score": r.BestScore,
|
||||
"avg_score": r.AvgScore,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.Payload(map[string]any{
|
||||
"total": total,
|
||||
"page": req.Page,
|
||||
"page_size": req.PageSize,
|
||||
"list": list,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetAdminGameRecords Admin查询扫雷对战记录
|
||||
// @Router /api/admin/games/records [get]
|
||||
func (h *handler) GetAdminGameRecords() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
var req struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
UserID int64 `form:"user_id"`
|
||||
MatchID string `form:"match_id"`
|
||||
}
|
||||
_ = ctx.ShouldBindQuery(&req)
|
||||
if req.Page <= 0 { req.Page = 1 }
|
||||
if req.PageSize <= 0 || req.PageSize > 100 { req.PageSize = 20 }
|
||||
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
|
||||
type row struct {
|
||||
ID int64 `json:"id"`
|
||||
MatchID string `json:"match_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
IsWinner bool `json:"is_winner"`
|
||||
RankPosition int `json:"rank_position"`
|
||||
TotalPlayers int `json:"total_players"`
|
||||
Score int `json:"score"`
|
||||
DamageDealt int `json:"damage_dealt"`
|
||||
ChestsCollected int `json:"chests_collected"`
|
||||
RankPoints int `json:"rank_points"`
|
||||
SettledAt string `json:"settled_at"`
|
||||
}
|
||||
|
||||
query := h.db.GetDbR().Table("minesweeper_game_records r").
|
||||
Select("r.id, r.match_id, r.user_id, COALESCE(u.nick_name,'') AS nickname, r.is_winner, r.rank_position, r.total_players, r.score, r.damage_dealt, r.chests_collected, r.rank_points, r.settled_at").
|
||||
Joins("LEFT JOIN users u ON u.id = r.user_id").
|
||||
Where("r.game_type = ?", "minesweeper")
|
||||
|
||||
if req.UserID > 0 {
|
||||
query = query.Where("r.user_id = ?", req.UserID)
|
||||
}
|
||||
if req.MatchID != "" {
|
||||
query = query.Where("r.match_id = ?", req.MatchID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var rows []row
|
||||
query.Order("r.settled_at DESC").Limit(req.PageSize).Offset(offset).Scan(&rows)
|
||||
|
||||
ctx.Payload(map[string]any{
|
||||
"total": total,
|
||||
"page": req.Page,
|
||||
"page_size": req.PageSize,
|
||||
"list": rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2 — 后端:注册新路由
|
||||
|
||||
**文件**: `internal/router/router.go`
|
||||
|
||||
在 admin 认证路由区域找到 game 相关路由,追加:
|
||||
```go
|
||||
adminAuthApiRouter.GET("/games/leaderboard", gameHandler.GetAdminLeaderboard())
|
||||
adminAuthApiRouter.GET("/games/records", gameHandler.GetAdminGameRecords())
|
||||
```
|
||||
|
||||
### Step 3 — 后端:SettleGame 去掉免费模式分支
|
||||
|
||||
**文件**: `internal/api/game/handler.go`
|
||||
|
||||
`isFreeMode` 判断相关逻辑仍可保留(对 `minesweeper` 类型无影响),但移除文档/注释中所有 `minesweeper_free` 提及。
|
||||
实际上后端逻辑本身没问题,如果 Nakama 不再发送 `minesweeper_free` 类型就不会触发,无需修改业务逻辑。
|
||||
|
||||
### Step 4 — 前端:在 index.vue 新增"排行榜"Tab
|
||||
|
||||
**文件**: `web/admin/src/views/operations/minesweeper/index.vue`
|
||||
|
||||
#### 4.1 在 `<el-tabs>` 中新增两个 Tab pane(追加在"配置预览"之前)
|
||||
|
||||
```html
|
||||
<!-- 5. 排行榜 -->
|
||||
<el-tab-pane label="排行榜" name="leaderboard">
|
||||
<div class="tab-content">
|
||||
<el-alert
|
||||
title="对战分说明:对战分是游戏内部的排名积分,与平台积分(商城积分/兑换积分)无关。赢得对局可获得更多对战分,用于在此排行榜中排名。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
show-icon
|
||||
/>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<el-input
|
||||
v-model="lbSearch"
|
||||
placeholder="搜索玩家昵称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@change="fetchLeaderboard"
|
||||
/>
|
||||
<el-button @click="fetchLeaderboard">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="lbList" border stripe v-loading="lbLoading">
|
||||
<el-table-column label="排名" width="70" align="center">
|
||||
<template #default="scope">
|
||||
<span :class="scope.row.rank <= 3 ? 'font-bold text-yellow-600' : ''">
|
||||
{{ scope.row.rank }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="玩家" min-width="140">
|
||||
<template #default="scope">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-avatar :src="scope.row.avatar" :size="28" v-if="scope.row.avatar" />
|
||||
<span>{{ scope.row.nickname || scope.row.user_id }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对战分" prop="total_rank_points" width="100" align="right" sortable />
|
||||
<el-table-column label="场次" prop="matches_played" width="80" align="center" />
|
||||
<el-table-column label="胜场" prop="wins" width="70" align="center" />
|
||||
<el-table-column label="胜率" width="80" align="center">
|
||||
<template #default="scope">{{ (scope.row.win_rate * 100).toFixed(1) }}%</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最高分" prop="best_score" width="90" align="right" />
|
||||
<el-table-column label="平均分" prop="avg_score" width="90" align="right">
|
||||
<template #default="scope">{{ Number(scope.row.avg_score).toFixed(1) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-3">
|
||||
<el-pagination
|
||||
v-model:current-page="lbPage"
|
||||
v-model:page-size="lbPageSize"
|
||||
:total="lbTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="fetchLeaderboard"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 6. 对战记录 -->
|
||||
<el-tab-pane label="对战记录" name="records">
|
||||
<div class="tab-content">
|
||||
<div class="flex gap-2 mb-4">
|
||||
<el-input
|
||||
v-model="recUserID"
|
||||
placeholder="按用户ID筛选"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@change="fetchRecords"
|
||||
/>
|
||||
<el-input
|
||||
v-model="recMatchID"
|
||||
placeholder="按局ID筛选"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@change="fetchRecords"
|
||||
/>
|
||||
<el-button @click="fetchRecords">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="recList" border stripe v-loading="recLoading" size="small">
|
||||
<el-table-column label="局ID" prop="match_id" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="玩家" min-width="120">
|
||||
<template #default="scope">{{ scope.row.nickname || scope.row.user_id }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="70" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.is_winner ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.is_winner ? '胜' : '败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名次" prop="rank_position" width="65" align="center" />
|
||||
<el-table-column label="总人数" prop="total_players" width="75" align="center" />
|
||||
<el-table-column label="得分" prop="score" width="80" align="right" />
|
||||
<el-table-column label="对战分" prop="rank_points" width="85" align="right" />
|
||||
<el-table-column label="结算时间" prop="settled_at" width="160" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-3">
|
||||
<el-pagination
|
||||
v-model:current-page="recPage"
|
||||
v-model:page-size="recPageSize"
|
||||
:total="recTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="fetchRecords"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
```
|
||||
|
||||
#### 4.2 在 `<script setup>` 中追加响应式数据和 API 函数
|
||||
|
||||
```typescript
|
||||
import request from '@/utils/http'
|
||||
|
||||
// 排行榜
|
||||
const lbSearch = ref('')
|
||||
const lbLoading = ref(false)
|
||||
const lbList = ref<any[]>([])
|
||||
const lbTotal = ref(0)
|
||||
const lbPage = ref(1)
|
||||
const lbPageSize = ref(20)
|
||||
|
||||
const fetchLeaderboard = async () => {
|
||||
lbLoading.value = true
|
||||
try {
|
||||
const res = await request.get('/admin/games/leaderboard', {
|
||||
params: { page: lbPage.value, page_size: lbPageSize.value, nickname: lbSearch.value }
|
||||
})
|
||||
lbList.value = res.list || []
|
||||
lbTotal.value = res.total || 0
|
||||
} finally {
|
||||
lbLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 对战记录
|
||||
const recUserID = ref('')
|
||||
const recMatchID = ref('')
|
||||
const recLoading = ref(false)
|
||||
const recList = ref<any[]>([])
|
||||
const recTotal = ref(0)
|
||||
const recPage = ref(1)
|
||||
const recPageSize = ref(20)
|
||||
|
||||
const fetchRecords = async () => {
|
||||
recLoading.value = true
|
||||
try {
|
||||
const res = await request.get('/admin/games/records', {
|
||||
params: {
|
||||
page: recPage.value,
|
||||
page_size: recPageSize.value,
|
||||
user_id: recUserID.value || undefined,
|
||||
match_id: recMatchID.value || undefined,
|
||||
}
|
||||
})
|
||||
recList.value = res.list || []
|
||||
recTotal.value = res.total || 0
|
||||
} finally {
|
||||
recLoading.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 在 `onMounted` 中追加调用
|
||||
|
||||
```typescript
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
fetchLeaderboard()
|
||||
fetchRecords()
|
||||
})
|
||||
```
|
||||
|
||||
#### 4.4 监听 Tab 切换(可选优化)
|
||||
|
||||
在 Tab 切换到对应 Tab 时按需加载,避免首次全量请求:
|
||||
```typescript
|
||||
watch(activeTab, (val) => {
|
||||
if (val === 'leaderboard') fetchLeaderboard()
|
||||
if (val === 'records') fetchRecords()
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键文件汇总
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `internal/api/game/handler.go` | 修改 | 新增 `GetAdminLeaderboard()` 和 `GetAdminGameRecords()` 两个函数 |
|
||||
| `internal/router/router.go` | 修改 | 注册两条新路由 |
|
||||
| `web/admin/src/views/operations/minesweeper/index.vue` | 修改 | 新增排行榜和对战记录两个 Tab + script 逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- "积分"含义:页面上 `total_rank_points` 显示为"对战分",并加 Alert 说明,避免与平台积分(points/兑换币)混淆
|
||||
- 不需要新建文件,全部在现有文件中追加
|
||||
- 后端无需修改任何免费模式的业务逻辑;前端 Tab 中不再展示 game_type 切换选项即可
|
||||
- `activeTab` 初始值改为 `'board'`(已经是),排行榜 Tab 不作为默认 Tab
|
||||
- 分页默认每页 20 条
|
||||
|
||||
---
|
||||
|
||||
## SESSION_ID
|
||||
- CODEX_SESSION: N/A(本次直接由 Claude 规划,无外部模型调用)
|
||||
- GEMINI_SESSION: N/A
|
||||
@ -1,142 +0,0 @@
|
||||
# 任务中心领取 Bug 修复计划
|
||||
|
||||
## 问题描述
|
||||
小程序(bindbox-mini)任务中心,活动前端领取不了但是可以看到。
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 核心问题:前后端进度数据源不一致
|
||||
|
||||
**后端 `ClaimTier`**(service.go:738)使用 `TierProgressMap`(基于窗口化、受任务时间约束的进度)来校验是否达标:
|
||||
```go
|
||||
if tp, ok := progress.TierProgressMap[tierID]; ok {
|
||||
currentOrderCount = tp.OrderCount // 窗口化进度,受任务 StartTime 约束
|
||||
}
|
||||
```
|
||||
|
||||
**前端 `isTierClaimable`**(index.vue:387-437)使用的是 `subProgress`(活动级别汇总)或全局进度(`orderCount` / `orderAmount`),**完全没有使用 `tier_progress_map`**。
|
||||
|
||||
### 触发条件
|
||||
|
||||
最近提交 `e0db875` 修改了 `computeTimeWindow`:
|
||||
- **修改前**:`lifetime` / `since_registration` / 空窗口 → `return nil, nil`(不限时间)
|
||||
- **修改后**:`lifetime` / 默认窗口 → `return taskStart, taskEnd`(受任务时间约束)
|
||||
|
||||
这导致 `TierProgressMap` 中的进度值被任务时间限制,但 API 返回的 `order_count` / `sub_progress` 全局进度仍然不受时间限制(service.go:618-622 用 `nil, nil` 查询)。
|
||||
|
||||
### 不一致的结果
|
||||
|
||||
| 数据源 | 时间约束 | 进度值 | 使用方 |
|
||||
|--------|---------|--------|--------|
|
||||
| `TierProgressMap` | 受任务 StartTime/EndTime 约束 | 较小 | 后端 ClaimTier |
|
||||
| `SubProgress` / 全局进度 | 无时间约束 | 较大 | 前端 isTierClaimable |
|
||||
| API 返回的 `order_count` | 无时间约束(或活动级别) | 较大 | 前端 isTierClaimable |
|
||||
|
||||
**场景举例**:
|
||||
- 用户在任务创建前有 5 笔历史订单,任务创建后有 2 笔新订单
|
||||
- 任务档位要求 `order_count >= 3`
|
||||
- 前端看到全局 `orderCount = 7`(不限时间) → 显示"领取"按钮
|
||||
- 后端 `TierProgressMap.OrderCount = 2`(只统计任务开始后) → 返回"任务条件未达成"
|
||||
|
||||
**或者反过来**:
|
||||
- 前端也使用 `subProgress` 做判断,但 `subProgress` 的统计可能不包含某些场景的数据
|
||||
- 导致前端 `isTierClaimable` 返回 `false`,按钮不出现
|
||||
- 用户看到任务但无法领取
|
||||
|
||||
## 任务类型
|
||||
- [x] 后端 (→ 后端逻辑修复)
|
||||
- [x] 前端 (→ 前端判断修复)
|
||||
|
||||
## 技术方案
|
||||
|
||||
**方案 A(推荐):后端 API 返回 `tier_progress_map`,前端使用**
|
||||
|
||||
让前后端使用同一份进度数据源(`TierProgressMap`),确保判断一致。
|
||||
|
||||
### 实施步骤
|
||||
|
||||
#### Step 1:后端 - API Response 增加 `tier_progress_map` 字段
|
||||
|
||||
**文件**: `internal/api/task_center/tasks_app.go`
|
||||
|
||||
1. 在 `taskProgressResponse` 结构体中添加 `TierProgressMap` 字段:
|
||||
```go
|
||||
type tierProgressItem struct {
|
||||
TierID int64 `json:"tier_id"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
OrderAmount int64 `json:"order_amount"`
|
||||
InviteCount int64 `json:"invite_count"`
|
||||
FirstOrder bool `json:"first_order"`
|
||||
}
|
||||
|
||||
type taskProgressResponse struct {
|
||||
// ... existing fields ...
|
||||
TierProgress []tierProgressItem `json:"tier_progress"` // 新增
|
||||
}
|
||||
```
|
||||
|
||||
2. 在 `GetTaskProgressForApp` handler 中填充该字段。
|
||||
|
||||
#### Step 2:前端 - `isTierClaimable` 优先使用 `tier_progress`
|
||||
|
||||
**文件**: `bindbox-mini/pages-user/tasks/index.vue`
|
||||
|
||||
1. 在 `fetchData` 中解析并存储 `tier_progress` 到 `taskProgress[taskId]`
|
||||
2. 修改 `isTierClaimable` 函数,优先从 `tierProgress` 中查找对应 tier 的进度
|
||||
3. 修改 `getTierProgressText` 和 `getTierProgressPercent`,同步使用新数据源
|
||||
|
||||
```js
|
||||
function isTierClaimable(task, tier) {
|
||||
const progress = taskProgress[task.id] || {}
|
||||
|
||||
// 优先使用 tier 级别窗口化进度(与后端 ClaimTier 保持一致)
|
||||
if (progress.tierProgress) {
|
||||
const tp = progress.tierProgress.find(t => t.tier_id === tier.id)
|
||||
if (tp) {
|
||||
const metric = tier.metric || ''
|
||||
const threshold = tier.threshold || 0
|
||||
const operator = tier.operator || '>='
|
||||
|
||||
let current = 0
|
||||
if (metric === 'first_order') return tp.first_order || false
|
||||
else if (metric === 'order_count') current = tp.order_count || 0
|
||||
else if (metric === 'order_amount') current = tp.order_amount || 0
|
||||
else if (metric === 'invite_count') current = tp.invite_count || 0
|
||||
|
||||
if (operator === '>=') return current >= threshold
|
||||
if (operator === '==') return current === threshold
|
||||
if (operator === '>') return current > threshold
|
||||
return current >= threshold
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: 原有逻辑
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3:同步修改进度显示
|
||||
|
||||
修改 `getTierProgressText` 和 `getTierProgressPercent` 也优先使用 `tierProgress` 数据,确保用户看到的进度和可领取状态一致。
|
||||
|
||||
### 关键文件
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `internal/api/task_center/tasks_app.go:106-170` | 修改 | 添加 tier_progress 到响应体 |
|
||||
| `bindbox-mini/pages-user/tasks/index.vue:387-437` | 修改 | isTierClaimable 使用 tier_progress |
|
||||
| `bindbox-mini/pages-user/tasks/index.vue:440-478` | 修改 | getTierProgressText 使用 tier_progress |
|
||||
| `bindbox-mini/pages-user/tasks/index.vue:590-630` | 修改 | getTierProgressPercent 使用 tier_progress |
|
||||
| `bindbox-mini/pages-user/tasks/index.vue:551-576` | 修改 | fetchData 解析 tier_progress |
|
||||
|
||||
### 风险与缓解
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| 前端旧版本未使用 `tier_progress` 字段 | 保持原有 `order_count`、`sub_progress` 字段不变,`tier_progress` 为新增字段,向后兼容 |
|
||||
| `tier_progress_map` 为空(数据库无 tiers 配置) | 前端 fallback 到原有 `subProgress` / 全局进度逻辑 |
|
||||
| 已部署但未刷新前端的用户 | `tier_progress` 是附加字段,不影响旧逻辑 |
|
||||
|
||||
### SESSION_ID(供 /ccg:execute 使用)
|
||||
- CODEX_SESSION: N/A(未使用外部模型)
|
||||
- GEMINI_SESSION: N/A(未使用外部模型)
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@ -27,6 +27,7 @@ go.work.sum
|
||||
resources/*
|
||||
build/resources/admin/
|
||||
logs/
|
||||
web/*
|
||||
|
||||
# 敏感配置文件
|
||||
configs/*.toml
|
||||
@ -36,17 +37,3 @@ configs/*.toml
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Codex local configuration
|
||||
.codex/
|
||||
|
||||
# Claude Flow runtime data
|
||||
.claude-flow/data/
|
||||
.claude-flow/logs/
|
||||
.planning
|
||||
.gocache
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
@ -1 +0,0 @@
|
||||
./inflections.go
|
||||
@ -1 +0,0 @@
|
||||
v1 0027d7b9e48cc563856ebbe018ca392c0380191db7057bd5c6ad5590058e19af f6460e601786f316ed501e85d3f6d89d20e99f1cfe32edc0fd4c1f38fdceceef 479 1772341459770076000
|
||||
@ -1 +0,0 @@
|
||||
v1 004248d4af5c24fab846e4f0d35d33273304315843bd4bb956274801c010da24 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341463855836000
|
||||
@ -1,14 +0,0 @@
|
||||
./assertI2I.go
|
||||
./base64_compat.go
|
||||
./fastconv.go
|
||||
./fastmem.go
|
||||
./fastvalue.go
|
||||
./gcwb.go
|
||||
./growslice.go
|
||||
./int48.go
|
||||
./map_siwss_go124.go
|
||||
./pool.go
|
||||
./stubs.go
|
||||
./table.go
|
||||
./types.go
|
||||
./asm_compat.s
|
||||
@ -1 +0,0 @@
|
||||
v1 004b0af6c36d9d8eed8c2a29a6afed52c277aa05814050dadb080d5eb41f579b dc7445418ed2fd747b88073bcada36fe086a7aa634c2740b36bfcbbacae1385d 394824 1772341466498068000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 005ae50987ddaed418137eefeeeab4c9746917594fd3496111f2d1a0f0840c59 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341462985082000
|
||||
@ -1 +0,0 @@
|
||||
v1 005e1f097c25fbb93dc6bffcc2336e31c331cb4add48da218bd708d97b5be992 09730b6a56d68e766986704d7aedb5e784b7523e18ebe04f8e8c850456e9c903 98 1772341460330220000
|
||||
@ -1 +0,0 @@
|
||||
v1 0060b867c320ba7a126368965bc597d818357f24552232f6374e7ffbbb4407e0 ddac1224448d22823a11fbf9f38bfa78d458ec7ec898b1ca8eab41746f442487 266 1772341463202328000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 0078588be74d4a9dc77e5add59427c71e6714a1a1398323ead99bd74a45b0138 1b5f10e5da3d839eda22be2bf43bdaf80a5a731bca3d99f3a1e5e0488bd7f1c3 646252 1772341462868690000
|
||||
@ -1 +0,0 @@
|
||||
v1 00798196f22bc20f9a85fabf7ced346cb9b41c1f3136e0e787b6830c21e6c874 879e941c6cff8537553a4bd74a1249ca03d45c9b709e3096b54edb62ddd5a584 293 1772341464123573000
|
||||
@ -1 +0,0 @@
|
||||
v1 0080a2d3c0dcbc6ab481304bd643e8dc47b59b93f95f71045591987cd48631dc e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341464615696000
|
||||
@ -1 +0,0 @@
|
||||
v1 008347c54f7aad0f3d604bcf61e24928140c90fc3ae9fe64a606c8db4bffec00 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341464681487000
|
||||
@ -1 +0,0 @@
|
||||
v1 008683d681b3adf0ad5b3cc15a3c02c64451d040a01702537482312d36745c9b 67b875e647c12268b021f5bdbbcbaeaaa961fbccad48dd5bf479eb164f5eedfa 1921554 1772341462767691000
|
||||
@ -1 +0,0 @@
|
||||
v1 00bb624d0a7863689f2d2cc0b82b036fbfcb11a6e1c60d29f7e4cefd7042bd4b e65d2f3cd83f084b7a3ef4f33d45d8c56f513e2f3b9fbf5719b3b14c30ae6cbd 715 1772341459969002000
|
||||
@ -1 +0,0 @@
|
||||
v1 00c45c3fa85a39f067f0624fdb8dc9609afd078f7f0b6b6e14343b503cafca50 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341462018876000
|
||||
@ -1 +0,0 @@
|
||||
v1 00d2eb94e953b271596fd0528682f0ece656ac8d73b0445e62ac9914ba31563b cfc12b3c4d18b5887b2c07c787b4dbf5483bc7562617f716e857f596253e5f3a 276778 1772341464207407000
|
||||
@ -1 +0,0 @@
|
||||
./godebug.go
|
||||
Binary file not shown.
@ -1,6 +0,0 @@
|
||||
./ast.go
|
||||
./builder.go
|
||||
./doc.go
|
||||
./kind.go
|
||||
./parser.go
|
||||
./scanner.go
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 011ebf6bc843f99ab28d15146048dfeee2c62fd1067e75a378bb9d1e62662dfa f9cc50b4663366c39cac8d6ed0b9fb1474a55b99912b0ed32a15697684b7805e 1200866 1772341460375940000
|
||||
@ -1 +0,0 @@
|
||||
v1 011f87c0ad369852441f4ddda1e1a2b415388f6abd647296bdcba6b5405ee367 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341463600224000
|
||||
@ -1 +0,0 @@
|
||||
v1 0125f1c08af9d4c3cc94f391170106327590885a29ea1a84f038f17001dbaac2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341464094428000
|
||||
@ -1 +0,0 @@
|
||||
v1 01615a2ad2cbba91aa9ffe6986e743f305273c4989f856992c98b0c89d1ea1d8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341464358733000
|
||||
@ -1 +0,0 @@
|
||||
v1 016ea07c95ecc6b071a34aafae05bb2b38313eb5802f329eb623211d7ef195e5 34a2943b8794a30f40e7dd9296858be67407669b4f83cab2897fac32b9cb9bfc 1868056 1772341464879061000
|
||||
@ -1 +0,0 @@
|
||||
./ignorepc.go
|
||||
@ -1 +0,0 @@
|
||||
v1 0194c6901fd8dec2ff9b3ef65cefc33e4fca04a0089c762705fa5f92bb95a850 b5699f899979d2d681bcec8b297aa093255ab952f4729f169ec17e904ebadb56 288080 1772341460310223000
|
||||
@ -1 +0,0 @@
|
||||
v1 01a754f07601ceca17362c314b58dd9d0e16644f2ebccc2e5309443d076b5b1c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341462939766000
|
||||
@ -1 +0,0 @@
|
||||
v1 01d1f64adfaa4373ca9c6dee7f74fa361e9a79c913540b3e7c679e52e98578f9 b1d1850a47832d0d06a1adde71436cad248080d16292f5a2776575c09f29f384 697 1772341459850231000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 01fd13c8028899be8f5581c7e09513b7246f08c3d8ebf334a508768099e6c7dc e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341462198596000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 024c0d5b779fbfa407d58b0596f7534977efb91f52c1a48d5b1d862bea568acb 1214475e07c7435a359c50c4c9f5aa43e1e75f70eea995a1b4f1669e3d0a0af2 162372 1772341462019400000
|
||||
@ -1 +0,0 @@
|
||||
v1 025195ca10e32a45a9142a7c922b0a194a65dfa0bb6a8514c56a1c8bfd88d21a c498fe8624736d651ee1e65f55e866ad53b0a55a641079dd1d1029bd0fcb08b6 77206 1772341463065444000
|
||||
Binary file not shown.
@ -1,10 +0,0 @@
|
||||
./format.go
|
||||
./format_rfc3339.go
|
||||
./sleep.go
|
||||
./sys_unix.go
|
||||
./tick.go
|
||||
./time.go
|
||||
./zoneinfo.go
|
||||
./zoneinfo_goroot.go
|
||||
./zoneinfo_read.go
|
||||
./zoneinfo_unix.go
|
||||
@ -1 +0,0 @@
|
||||
v1 0276b3e6ca37051cb11b5854546c02b27bb187c4a63ecaf2e115effc3237d670 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341462858338000
|
||||
@ -1 +0,0 @@
|
||||
v1 02807f1cf2431b48f57bd53071c04d1bb9bd26278a12672e15855de0aa3a1bc8 3a5cdc824bca6444dc87e8e57d4792ddcdfb38c62ea6008263424384d4f4fe48 73 1772341462621509000
|
||||
@ -1,2 +0,0 @@
|
||||
./cors.go
|
||||
./utils.go
|
||||
@ -1 +0,0 @@
|
||||
v1 02a69eeee669f96f852761f3a545dc13819721097192ebe1f7ee53411b83b571 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341462067569000
|
||||
@ -1 +0,0 @@
|
||||
v1 02a99851c63f34f71da6f1e2751b6662ade7ee472a6ef4cfa3b0f6c580908b4a ae5c5d1d4af6c8a4be3159a450324ac93e5e4393b0145ca7176c795dea50c2d3 684364 1772341463993922000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 02d705fe277d278e149b941bbe5242af8c38be0b93ac6a4398ad9851dcb56455 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341462118153000
|
||||
@ -1 +0,0 @@
|
||||
v1 02d71ca3fd2c793ee989faf0b7dbe2ca85e4560ed0abaf2964791f5b02c82eec ea8cb45d584d9384b541ad180ed362aa88f4a2634e904750dfc5db513a43a117 348 1772341459858832000
|
||||
Binary file not shown.
@ -1,42 +0,0 @@
|
||||
./adapter.go
|
||||
./any.go
|
||||
./any_array.go
|
||||
./any_bool.go
|
||||
./any_float.go
|
||||
./any_int32.go
|
||||
./any_int64.go
|
||||
./any_invalid.go
|
||||
./any_nil.go
|
||||
./any_number.go
|
||||
./any_object.go
|
||||
./any_str.go
|
||||
./any_uint32.go
|
||||
./any_uint64.go
|
||||
./config.go
|
||||
./iter.go
|
||||
./iter_array.go
|
||||
./iter_float.go
|
||||
./iter_int.go
|
||||
./iter_object.go
|
||||
./iter_skip.go
|
||||
./iter_skip_strict.go
|
||||
./iter_str.go
|
||||
./jsoniter.go
|
||||
./pool.go
|
||||
./reflect.go
|
||||
./reflect_array.go
|
||||
./reflect_dynamic.go
|
||||
./reflect_extension.go
|
||||
./reflect_json_number.go
|
||||
./reflect_json_raw_message.go
|
||||
./reflect_map.go
|
||||
./reflect_marshaler.go
|
||||
./reflect_native.go
|
||||
./reflect_optional.go
|
||||
./reflect_slice.go
|
||||
./reflect_struct_decoder.go
|
||||
./reflect_struct_encoder.go
|
||||
./stream.go
|
||||
./stream_float.go
|
||||
./stream_int.go
|
||||
./stream_str.go
|
||||
@ -1 +0,0 @@
|
||||
v1 02fc2b1b830b7ade9d77044adcad7a9a4a57cf1b0cf61eddb3741225043eb398 7716152837087e14b9bcd0e8f8d94bc6b573b7357ca1c497420f384461410349 133144 1772341463086740000
|
||||
@ -1 +0,0 @@
|
||||
v1 030b4ba59b5b43bfce4b42d558f0b803a73a28a6e8a9e9dc0a9d47a64e3a86a0 c3123b2c3800ce5313dac4be625b9f86c476e2a8c2f118527338aec1bb54ae5f 3694 1772341459982335000
|
||||
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 034e1dc78deebe692796527c71d18c45651cc6ca896e9d0a73571c4009ef290c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341462145386000
|
||||
@ -1 +0,0 @@
|
||||
v1 035e9e05508c281231207c45087246c370a21b1413bde994f8807242452ec035 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341463007122000
|
||||
@ -1 +0,0 @@
|
||||
v1 03618d69e403c6a792cd0358b14a7eae5fe10476465c4d1925e2c35e36f79636 6d16aed1afc018b12829a094871b7e2bd30d654ca5cdf244da83dd983acc51fd 9 1772341465718120000
|
||||
@ -1 +0,0 @@
|
||||
v1 03673185c07dbd6aa5c58edea0ebd836d10bfcbcc880ca9d3b08f4ae2d62dd7d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341460253738000
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 039b8f45a6e9c3167c7c2d224cfc333464a10acf4fd3c69aec98187269b43591 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341463010982000
|
||||
@ -1 +0,0 @@
|
||||
v1 03a0f2517ee71e2152a17a8e20df068b6799a30acbca4ab7563f0db9d88a60fe 1d647cfb2a3507b6201a30bb975bdf620015e9fdbae85128a598765722483bba 618738 1772341463077954000
|
||||
@ -1 +0,0 @@
|
||||
v1 03ad22550bf62742af275db9cf8ecaca8d22dd3de471fcecb4aeacd6802d30b5 28705ba6a6c4d9fa91033f0d45b624a414786fc37b2046774eee672e5ddfef75 1110 1772341459772488000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 03cd960e6780f47a1bbe2de71bfcfd9f77bcfd387a1db10f634287a7d767d4b2 3b78e01ec9aeb35516dad96f495cfddd5ceed02f9483ca0c018570cbeaf1ed56 2536 1772341460056872000
|
||||
@ -1 +0,0 @@
|
||||
v1 03d54565ae2ef9fdbb6b310f1c1b435c713b7b605b91203c6c22ab3b49908cd2 6060af58c3467f320159a76b5e59f64738ce7264d6483b229d14f6b9a3dc1aaf 259738 1772341463973434000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 03dab1b1cc7ad33783573189a6a902adfc7e092be9e3e33588aa69b274b7965b 47a532157373b8eb3154f788683cb2875e9561016af49dccd66c6c6c96a98d10 3181 1772341459968904000
|
||||
@ -1 +0,0 @@
|
||||
v1 04082d8c4ed8aa82b26d6fbc789426eed6ec934ae9a728bc8ec087cac278b2bc 2a6491b22e434d3ec3c7a0d23c093a0f100f092af19cd8592ee392d185344d1c 784 1772341462997469000
|
||||
@ -1 +0,0 @@
|
||||
v1 042075ee4b23c849788b99bf5418546d81747010488758b8e93ab4099bbf093c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341460247772000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 042d0543a4162599bb79b6daeb070c61d24406ae9ecd8abdd6c47af7b297d4e2 341ef2f104587e3ca031596f710653b08ee23b870a761adfd896b3aed56175fe 163870 1772341463486813000
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
v1 044a459d3867e21a5cdd80185e95689596d4154a4ed6901f6a2ec0f959d46264 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1772341460301399000
|
||||
@ -1 +0,0 @@
|
||||
v1 0462f97d5bc45b14a1bf6263658ef502c80a7d5aba156d95e783060e89584913 5c009450a074dea7546b45e3dff2f1c67c36fa5ba0ee94ddd010e18270d23423 209178 1772341461479908000
|
||||
@ -1 +0,0 @@
|
||||
v1 046397a7f62b5b725c637268d57ab265ee043ed99b0d1c62aa4d2b67ad73248b ee3fb04d9eabdfa66ab70e6f68d2a6ebff1c0dd7c4c7cdcf219199e453b18580 313 1772341464785664000
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user