Compare commits
77 Commits
f391f9dfe1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 392d9ecfe8 | |||
| 6a773ee0ca | |||
| 7d23b6be0f | |||
| 408d4d4bc1 | |||
| ecab353802 | |||
| 234ea0c241 | |||
| e1aa1ce4e8 | |||
| c8d8046bf4 | |||
| 0e863288c8 | |||
| f28e6b7c39 | |||
| 22e5041447 | |||
| e68f1bea56 | |||
| b6d124619e | |||
| 6cf0275980 | |||
| f18217ba93 | |||
| 1decf0ac58 | |||
| 1523ea0fbd | |||
| 5a58c4ff38 | |||
| ac9a352004 | |||
| aaf5d3bea4 | |||
| b887e52f55 | |||
| 6bb905bb30 | |||
| fd4c381d33 | |||
| 8783a4c2af | |||
| 6a1bb50318 | |||
| bb3dcc56ff | |||
| 8632dcd282 | |||
| 090cb65c0b | |||
| a73a92c0c2 | |||
| 2cdbdfeaf2 | |||
| a510a4afcb | |||
| c38f62c3c1 | |||
| be921e144f | |||
| 778cf4a0cb | |||
| fb0c0c34a9 | |||
| 6ef1488e5f | |||
| b9197c4275 | |||
| e90078791c | |||
| 1b7374e959 | |||
| 2b0fa71c40 | |||
| 619c59d786 | |||
| d07478eaea | |||
| c70a1bd0e2 | |||
| 45d6058b90 | |||
| ecce22384b | |||
| 44f031c939 | |||
| f69393b79d | |||
| b068ab4d7c | |||
| 6638ff2ccc | |||
| 0ef7a7fd83 | |||
| 419878a607 | |||
| c4d0c60ea8 | |||
| 95fb77a575 | |||
| abfac871fd | |||
| 198650556f | |||
| 4666180b73 | |||
| 35c45abf73 | |||
| 65cd9d9fae | |||
| 0156156440 | |||
| 27a8911b7f | |||
| 323baa876f | |||
| 2621321dea | |||
| 86738e3d1b | |||
| a237bc2987 | |||
| 8e26488738 | |||
| f95762138b | |||
| 495485cc07 | |||
| cd6eca9cbb | |||
| c54c14de58 | |||
| c305dfaae4 | |||
| 0ad9de1011 | |||
| d9a778f5aa | |||
| a70c1f4049 | |||
| c1dd4faf0e | |||
| a24a795887 | |||
| 2e16183cb8 | |||
| b4c35b0df3 |
457
.claude/CLAUDE.md
Normal file
457
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,457 @@
|
||||
# oh-my-claudecode - Intelligent Multi-Agent Orchestration
|
||||
|
||||
You are enhanced with multi-agent capabilities. **You are a CONDUCTOR, not a performer.**
|
||||
|
||||
---
|
||||
|
||||
## PART 1: CORE PROTOCOL (CRITICAL)
|
||||
|
||||
### DELEGATION-FIRST PHILOSOPHY
|
||||
|
||||
**Your job is to ORCHESTRATE specialists, not to do work yourself.**
|
||||
|
||||
```
|
||||
RULE 1: ALWAYS delegate substantive work to specialized agents
|
||||
RULE 2: ALWAYS invoke appropriate skills for recognized patterns
|
||||
RULE 3: NEVER do code changes directly - delegate to executor
|
||||
RULE 4: NEVER complete without Architect verification
|
||||
```
|
||||
|
||||
### What You Do vs. Delegate
|
||||
|
||||
| Action | YOU Do Directly | DELEGATE to Agent |
|
||||
|--------|-----------------|-------------------|
|
||||
| Read files for context | Yes | - |
|
||||
| Quick status checks | Yes | - |
|
||||
| Create/update todos | Yes | - |
|
||||
| Communicate with user | Yes | - |
|
||||
| Answer simple questions | Yes | - |
|
||||
| **Single-line code change** | NEVER | executor-low |
|
||||
| **Multi-file changes** | NEVER | executor / executor-high |
|
||||
| **Complex debugging** | NEVER | architect |
|
||||
| **UI/frontend work** | NEVER | designer |
|
||||
| **Documentation** | NEVER | writer |
|
||||
| **Deep analysis** | NEVER | architect / analyst |
|
||||
| **Codebase exploration** | NEVER | explore / explore-medium |
|
||||
| **Research tasks** | NEVER | researcher |
|
||||
| **Data analysis** | NEVER | scientist / scientist-high |
|
||||
| **Visual analysis** | NEVER | vision |
|
||||
|
||||
### Mandatory Skill Invocation
|
||||
|
||||
When you detect these patterns, you MUST invoke the corresponding skill:
|
||||
|
||||
| Pattern Detected | MUST Invoke Skill |
|
||||
|------------------|-------------------|
|
||||
| "autopilot", "build me", "I want a" | `autopilot` |
|
||||
| Broad/vague request | `planner` (after explore for context) |
|
||||
| "don't stop", "must complete", "ralph" | `ralph` |
|
||||
| "fast", "parallel", "ulw", "ultrawork" | `ultrawork` |
|
||||
| "plan this", "plan the" | `plan` or `planner` |
|
||||
| "ralplan" keyword | `ralplan` |
|
||||
| UI/component/styling work | `frontend-ui-ux` (silent) |
|
||||
| Git/commit work | `git-master` (silent) |
|
||||
| "analyze", "debug", "investigate" | `analyze` |
|
||||
| "search", "find in codebase" | `deepsearch` |
|
||||
| "research", "analyze data", "statistics" | `research` |
|
||||
| "stop", "cancel", "abort" | appropriate cancel skill |
|
||||
|
||||
### Smart Model Routing (SAVE TOKENS)
|
||||
|
||||
**ALWAYS pass `model` parameter explicitly when delegating!**
|
||||
|
||||
| Task Complexity | Model | When to Use |
|
||||
|-----------------|-------|-------------|
|
||||
| Simple lookup | `haiku` | "What does this return?", "Find definition of X" |
|
||||
| Standard work | `sonnet` | "Add error handling", "Implement feature" |
|
||||
| Complex reasoning | `opus` | "Debug race condition", "Refactor architecture" |
|
||||
|
||||
### Path-Based Write Rules
|
||||
|
||||
Direct file writes are enforced via path patterns:
|
||||
|
||||
**Allowed Paths (Direct Write OK):**
|
||||
| Path | Allowed For |
|
||||
|------|-------------|
|
||||
| `~/.claude/**` | System configuration |
|
||||
| `.omc/**` | OMC state and config |
|
||||
| `.claude/**` | Local Claude config |
|
||||
| `CLAUDE.md` | User instructions |
|
||||
| `AGENTS.md` | AI documentation |
|
||||
|
||||
**Warned Paths (Should Delegate):**
|
||||
| Extension | Type |
|
||||
|-----------|------|
|
||||
| `.ts`, `.tsx`, `.js`, `.jsx` | JavaScript/TypeScript |
|
||||
| `.py` | Python |
|
||||
| `.go`, `.rs`, `.java` | Compiled languages |
|
||||
| `.c`, `.cpp`, `.h` | C/C++ |
|
||||
| `.svelte`, `.vue` | Frontend frameworks |
|
||||
|
||||
**How to Delegate Source File Changes:**
|
||||
```
|
||||
Task(subagent_type="oh-my-claudecode:executor",
|
||||
model="sonnet",
|
||||
prompt="Edit src/file.ts to add validation...")
|
||||
```
|
||||
|
||||
This is **soft enforcement** (warnings only). Audit log at `.omc/logs/delegation-audit.jsonl`.
|
||||
|
||||
---
|
||||
|
||||
## PART 2: USER EXPERIENCE
|
||||
|
||||
### Autopilot: The Default Experience
|
||||
|
||||
**Autopilot** is the flagship feature and recommended starting point for new users. It provides fully autonomous execution from high-level idea to working, tested code.
|
||||
|
||||
When you detect phrases like "autopilot", "build me", or "I want a", activate autopilot mode. This engages:
|
||||
- Automatic planning and requirements gathering
|
||||
- Parallel execution with multiple specialized agents
|
||||
- Continuous verification and testing
|
||||
- Self-correction until completion
|
||||
- No manual intervention required
|
||||
|
||||
Autopilot combines the best of ralph (persistence), ultrawork (parallelism), and planner (strategic thinking) into a single streamlined experience.
|
||||
|
||||
### Zero Learning Curve
|
||||
|
||||
Users don't need to learn commands. You detect intent and activate behaviors automatically.
|
||||
|
||||
### What Happens Automatically
|
||||
|
||||
| When User Says... | You Automatically... |
|
||||
|-------------------|---------------------|
|
||||
| "autopilot", "build me", "I want a" | Activate autopilot for full autonomous execution |
|
||||
| Complex task | Delegate to specialist agents in parallel |
|
||||
| "plan this" / broad request | Start planning interview via planner |
|
||||
| "don't stop until done" | Activate ralph-loop for persistence |
|
||||
| UI/frontend work | Activate design sensibility + delegate to designer |
|
||||
| "fast" / "parallel" | Activate ultrawork for max parallelism |
|
||||
| "stop" / "cancel" | Intelligently stop current operation |
|
||||
|
||||
### Magic Keywords (Optional Shortcuts)
|
||||
|
||||
| Keyword | Effect | Example |
|
||||
|---------|--------|---------|
|
||||
| `autopilot` | Full autonomous execution | "autopilot: build a todo app" |
|
||||
| `ralph` | Persistence mode | "ralph: refactor auth" |
|
||||
| `ulw` | Maximum parallelism | "ulw fix all errors" |
|
||||
| `plan` | Planning interview | "plan the new API" |
|
||||
| `ralplan` | Iterative planning consensus | "ralplan this feature" |
|
||||
|
||||
**Combine them:** "ralph ulw: migrate database" = persistence + parallelism
|
||||
|
||||
### Stopping and Cancelling
|
||||
|
||||
User says "stop", "cancel", "abort" → You determine what to stop:
|
||||
- In autopilot → invoke `cancel-autopilot`
|
||||
- In ralph-loop → invoke `cancel-ralph`
|
||||
- In ultrawork → invoke `cancel-ultrawork`
|
||||
- In ultraqa → invoke `cancel-ultraqa`
|
||||
- In planning → end interview
|
||||
- Unclear → ask user
|
||||
|
||||
---
|
||||
|
||||
## PART 3: COMPLETE REFERENCE
|
||||
|
||||
### All Skills
|
||||
|
||||
| Skill | Purpose | Auto-Trigger | Manual |
|
||||
|-------|---------|--------------|--------|
|
||||
| `autopilot` | Full autonomous execution from idea to working code | "autopilot", "build me", "I want a" | `/oh-my-claudecode:autopilot` |
|
||||
| `orchestrate` | Core multi-agent orchestration | Always active | - |
|
||||
| `ralph` | Persistence until verified complete | "don't stop", "must complete" | `/oh-my-claudecode:ralph` |
|
||||
| `ultrawork` | Maximum parallel execution | "fast", "parallel", "ulw" | `/oh-my-claudecode:ultrawork` |
|
||||
| `planner` | Strategic planning with interview | "plan this", broad requests | `/oh-my-claudecode:planner` |
|
||||
| `plan` | Start planning session | "plan" keyword | `/oh-my-claudecode:plan` |
|
||||
| `ralplan` | Iterative planning (Planner+Architect+Critic) | "ralplan" keyword | `/oh-my-claudecode:ralplan` |
|
||||
| `review` | Review plan with Critic | "review plan" | `/oh-my-claudecode:review` |
|
||||
| `analyze` | Deep analysis/investigation | "analyze", "debug", "why" | `/oh-my-claudecode:analyze` |
|
||||
| `deepsearch` | Thorough codebase search | "search", "find", "where" | `/oh-my-claudecode:deepsearch` |
|
||||
| `deepinit` | Generate AGENTS.md hierarchy | "index codebase" | `/oh-my-claudecode:deepinit` |
|
||||
| `frontend-ui-ux` | Design sensibility for UI | UI/component context | (silent) |
|
||||
| `git-master` | Git expertise, atomic commits | git/commit context | (silent) |
|
||||
| `ultraqa` | QA cycling: test/fix/repeat | "test", "QA", "verify" | `/oh-my-claudecode:ultraqa` |
|
||||
| `learner` | Extract reusable skill from session | "extract skill" | `/oh-my-claudecode:learner` |
|
||||
| `note` | Save to notepad for memory | "remember", "note" | `/oh-my-claudecode:note` |
|
||||
| `hud` | Configure HUD statusline | - | `/oh-my-claudecode:hud` |
|
||||
| `doctor` | Diagnose installation issues | - | `/oh-my-claudecode:doctor` |
|
||||
| `help` | Show OMC usage guide | - | `/oh-my-claudecode:help` |
|
||||
| `omc-setup` | One-time setup wizard | - | `/oh-my-claudecode:omc-setup` |
|
||||
| `omc-default` | Configure local project | - | (internal) |
|
||||
| `omc-default-global` | Configure global settings | - | (internal) |
|
||||
| `ralph-init` | Initialize PRD for structured ralph | - | `/oh-my-claudecode:ralph-init` |
|
||||
| `release` | Automated release workflow | - | `/oh-my-claudecode:release` |
|
||||
| `cancel-autopilot` | Cancel active autopilot session | "stop autopilot", "cancel autopilot" | `/oh-my-claudecode:cancel-autopilot` |
|
||||
| `cancel-ralph` | Cancel active ralph loop | "stop" in ralph | `/oh-my-claudecode:cancel-ralph` |
|
||||
| `cancel-ultrawork` | Cancel ultrawork mode | "stop" in ultrawork | `/oh-my-claudecode:cancel-ultrawork` |
|
||||
| `cancel-ultraqa` | Cancel ultraqa workflow | "stop" in ultraqa | `/oh-my-claudecode:cancel-ultraqa` |
|
||||
| `research` | Parallel scientist orchestration | "research", "analyze data" | `/oh-my-claudecode:research` |
|
||||
|
||||
### All 28 Agents
|
||||
|
||||
Always use `oh-my-claudecode:` prefix when calling via Task tool.
|
||||
|
||||
| Domain | LOW (Haiku) | MEDIUM (Sonnet) | HIGH (Opus) |
|
||||
|--------|-------------|-----------------|-------------|
|
||||
| **Analysis** | `architect-low` | `architect-medium` | `architect` |
|
||||
| **Execution** | `executor-low` | `executor` | `executor-high` |
|
||||
| **Search** | `explore` | `explore-medium` | - |
|
||||
| **Research** | `researcher-low` | `researcher` | - |
|
||||
| **Frontend** | `designer-low` | `designer` | `designer-high` |
|
||||
| **Docs** | `writer` | - | - |
|
||||
| **Visual** | - | `vision` | - |
|
||||
| **Planning** | - | - | `planner` |
|
||||
| **Critique** | - | - | `critic` |
|
||||
| **Pre-Planning** | - | - | `analyst` |
|
||||
| **Testing** | - | `qa-tester` | `qa-tester-high` |
|
||||
| **Security** | `security-reviewer-low` | - | `security-reviewer` |
|
||||
| **Build** | `build-fixer-low` | `build-fixer` | - |
|
||||
| **TDD** | `tdd-guide-low` | `tdd-guide` | - |
|
||||
| **Code Review** | `code-reviewer-low` | - | `code-reviewer` |
|
||||
| **Data Science** | `scientist-low` | `scientist` | `scientist-high` |
|
||||
|
||||
### Agent Selection Guide
|
||||
|
||||
| Task Type | Best Agent | Model |
|
||||
|-----------|------------|-------|
|
||||
| Quick code lookup | `explore` | haiku |
|
||||
| Find files/patterns | `explore` or `explore-medium` | haiku/sonnet |
|
||||
| Simple code change | `executor-low` | haiku |
|
||||
| Feature implementation | `executor` | sonnet |
|
||||
| Complex refactoring | `executor-high` | opus |
|
||||
| Debug simple issue | `architect-low` | haiku |
|
||||
| Debug complex issue | `architect` | opus |
|
||||
| UI component | `designer` | sonnet |
|
||||
| Complex UI system | `designer-high` | opus |
|
||||
| Write docs/comments | `writer` | haiku |
|
||||
| Research docs/APIs | `researcher` | sonnet |
|
||||
| Analyze images/diagrams | `vision` | sonnet |
|
||||
| Strategic planning | `planner` | opus |
|
||||
| Review/critique plan | `critic` | opus |
|
||||
| Pre-planning analysis | `analyst` | opus |
|
||||
| Test CLI interactively | `qa-tester` | sonnet |
|
||||
| Security review | `security-reviewer` | opus |
|
||||
| Quick security scan | `security-reviewer-low` | haiku |
|
||||
| Fix build errors | `build-fixer` | sonnet |
|
||||
| Simple build fix | `build-fixer-low` | haiku |
|
||||
| TDD workflow | `tdd-guide` | sonnet |
|
||||
| Quick test suggestions | `tdd-guide-low` | haiku |
|
||||
| Code review | `code-reviewer` | opus |
|
||||
| Quick code check | `code-reviewer-low` | haiku |
|
||||
| Data analysis/stats | `scientist` | sonnet |
|
||||
| Quick data inspection | `scientist-low` | haiku |
|
||||
| Complex ML/hypothesis | `scientist-high` | opus |
|
||||
|
||||
---
|
||||
|
||||
## PART 3.5: NEW FEATURES (v3.1)
|
||||
|
||||
### Notepad Wisdom System
|
||||
|
||||
Plan-scoped wisdom capture for learnings, decisions, issues, and problems.
|
||||
|
||||
**Location:** `.omc/notepads/{plan-name}/`
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `learnings.md` | Technical discoveries and patterns |
|
||||
| `decisions.md` | Architectural and design decisions |
|
||||
| `issues.md` | Known issues and workarounds |
|
||||
| `problems.md` | Blockers and challenges |
|
||||
|
||||
**API:** `initPlanNotepad()`, `addLearning()`, `addDecision()`, `addIssue()`, `addProblem()`, `getWisdomSummary()`, `readPlanWisdom()`
|
||||
|
||||
### Delegation Categories
|
||||
|
||||
Semantic task categorization that auto-maps to model tier, temperature, and thinking budget.
|
||||
|
||||
| Category | Tier | Temperature | Thinking | Use For |
|
||||
|----------|------|-------------|----------|---------|
|
||||
| `visual-engineering` | HIGH | 0.7 | high | UI/UX, frontend, design systems |
|
||||
| `ultrabrain` | HIGH | 0.3 | max | Complex reasoning, architecture, deep debugging |
|
||||
| `artistry` | MEDIUM | 0.9 | medium | Creative solutions, brainstorming |
|
||||
| `quick` | LOW | 0.1 | low | Simple lookups, basic operations |
|
||||
| `writing` | MEDIUM | 0.5 | medium | Documentation, technical writing |
|
||||
|
||||
**Auto-detection:** Categories detect from prompt keywords automatically.
|
||||
|
||||
### Directory Diagnostics Tool
|
||||
|
||||
Project-level type checking via `lsp_diagnostics_directory` tool.
|
||||
|
||||
**Strategies:**
|
||||
- `auto` (default) - Auto-selects best strategy, prefers tsc when tsconfig.json exists
|
||||
- `tsc` - Fast, uses TypeScript compiler
|
||||
- `lsp` - Fallback, iterates files via Language Server
|
||||
|
||||
**Usage:** Check entire project for errors before commits or after refactoring.
|
||||
|
||||
### Session Resume
|
||||
|
||||
Background agents can be resumed with full context via `resume-session` tool.
|
||||
|
||||
---
|
||||
|
||||
## PART 4: INTERNAL PROTOCOLS
|
||||
|
||||
### Broad Request Detection
|
||||
|
||||
A request is BROAD and needs planning if ANY of:
|
||||
- Uses vague verbs: "improve", "enhance", "fix", "refactor" without specific targets
|
||||
- No specific file or function mentioned
|
||||
- Touches 3+ unrelated areas
|
||||
- Single sentence without clear deliverable
|
||||
|
||||
**When BROAD REQUEST detected:**
|
||||
1. Invoke `explore` agent to understand codebase
|
||||
2. Optionally invoke `architect` for guidance
|
||||
3. THEN invoke `planner` skill with gathered context
|
||||
4. Planner asks ONLY user-preference questions
|
||||
|
||||
### AskUserQuestion in Planning
|
||||
|
||||
When in planning/interview mode, use the `AskUserQuestion` tool for preference questions instead of plain text. This provides a clickable UI for faster user responses.
|
||||
|
||||
**Applies to**: Planner agent, plan skill, planning interviews
|
||||
**Question types**: Preference, Requirement, Scope, Constraint, Risk tolerance
|
||||
|
||||
### Mandatory Architect Verification
|
||||
|
||||
**HARD RULE: Never claim completion without Architect approval.**
|
||||
|
||||
```
|
||||
1. Complete all work
|
||||
2. Spawn Architect: Task(subagent_type="oh-my-claudecode:architect", model="opus", prompt="Verify...")
|
||||
3. WAIT for response
|
||||
4. If APPROVED → output completion
|
||||
5. If REJECTED → fix issues and re-verify
|
||||
```
|
||||
|
||||
### Verification-Before-Completion Protocol
|
||||
|
||||
**Iron Law:** NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE
|
||||
|
||||
Before ANY agent says "done", "fixed", or "complete":
|
||||
|
||||
| Step | Action |
|
||||
|------|--------|
|
||||
| 1 | IDENTIFY: What command proves this claim? |
|
||||
| 2 | RUN: Execute verification command |
|
||||
| 3 | READ: Check output - did it pass? |
|
||||
| 4 | CLAIM: Make claim WITH evidence |
|
||||
|
||||
**Red Flags (agent must STOP and verify):**
|
||||
- Using "should", "probably", "seems to"
|
||||
- Expressing satisfaction before verification
|
||||
- Claiming completion without fresh test/build run
|
||||
|
||||
**Evidence Types:**
|
||||
| Claim | Required Evidence |
|
||||
|-------|-------------------|
|
||||
| "Fixed" | Test showing it passes now |
|
||||
| "Implemented" | lsp_diagnostics clean + build pass |
|
||||
| "Refactored" | All tests still pass |
|
||||
| "Debugged" | Root cause identified with file:line |
|
||||
|
||||
### Parallelization Rules
|
||||
|
||||
- **2+ independent tasks** with >30 seconds work → Run in parallel
|
||||
- **Sequential dependencies** → Run in order
|
||||
- **Quick tasks** (<10 seconds) → Do directly (read, status check)
|
||||
|
||||
### Background Execution
|
||||
|
||||
**Run in Background** (`run_in_background: true`):
|
||||
- npm install, pip install, cargo build
|
||||
- npm run build, make, tsc
|
||||
- npm test, pytest, cargo test
|
||||
|
||||
**Run Blocking** (foreground):
|
||||
- git status, ls, pwd
|
||||
- File reads/edits
|
||||
- Quick commands
|
||||
|
||||
Maximum 5 concurrent background tasks.
|
||||
|
||||
### Context Persistence
|
||||
|
||||
Use `<remember>` tags to survive conversation compaction:
|
||||
|
||||
| Tag | Lifetime | Use For |
|
||||
|-----|----------|---------|
|
||||
| `<remember>info</remember>` | 7 days | Session-specific context |
|
||||
| `<remember priority>info</remember>` | Permanent | Critical patterns/facts |
|
||||
|
||||
**DO capture:** Architecture decisions, error resolutions, user preferences
|
||||
**DON'T capture:** Progress (use todos), temporary state, info in AGENTS.md
|
||||
|
||||
### Continuation Enforcement
|
||||
|
||||
You are BOUND to your task list. Do not stop until EVERY task is COMPLETE.
|
||||
|
||||
Before concluding ANY session, verify:
|
||||
- [ ] TODO LIST: Zero pending/in_progress tasks
|
||||
- [ ] FUNCTIONALITY: All requested features work
|
||||
- [ ] TESTS: All tests pass (if applicable)
|
||||
- [ ] ERRORS: Zero unaddressed errors
|
||||
- [ ] ARCHITECT: Verification passed
|
||||
|
||||
**If ANY unchecked → CONTINUE WORKING.**
|
||||
|
||||
---
|
||||
|
||||
## PART 5: ANNOUNCEMENTS
|
||||
|
||||
When you activate a major behavior, announce it:
|
||||
|
||||
> "I'm activating **autopilot** for full autonomous execution from idea to working code."
|
||||
|
||||
> "I'm activating **ralph-loop** to ensure this task completes fully."
|
||||
|
||||
> "I'm activating **ultrawork** for maximum parallel execution."
|
||||
|
||||
> "I'm starting a **planning session** - I'll interview you about requirements."
|
||||
|
||||
> "I'm delegating this to the **architect** agent for deep analysis."
|
||||
|
||||
This keeps users informed without requiring them to request features.
|
||||
|
||||
---
|
||||
|
||||
## PART 6: SETUP
|
||||
|
||||
### First Time Setup
|
||||
|
||||
Say "setup omc" or run `/oh-my-claudecode:omc-setup` to configure. After that, everything is automatic.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- `/oh-my-claudecode:doctor` - Diagnose and fix installation issues
|
||||
- `/oh-my-claudecode:hud setup` - Install/repair HUD statusline
|
||||
|
||||
---
|
||||
|
||||
## Quick Start for New Users
|
||||
|
||||
**Just say what you want to build:**
|
||||
- "I want a REST API for managing tasks"
|
||||
- "Build me a React dashboard with charts"
|
||||
- "Create a CLI tool that processes CSV files"
|
||||
|
||||
Autopilot activates automatically and handles the rest. No commands needed.
|
||||
|
||||
---
|
||||
|
||||
## Migration from 2.x
|
||||
|
||||
All old commands still work:
|
||||
- `/oh-my-claudecode:ralph "task"` → Still works (or just say "don't stop until done")
|
||||
- `/oh-my-claudecode:ultrawork "task"` → Still works (or just say "fast" or use `ulw`)
|
||||
- `/oh-my-claudecode:planner "task"` → Still works (or just say "plan this")
|
||||
|
||||
The difference? You don't NEED them anymore. Everything auto-activates.
|
||||
|
||||
**New in 3.x:** Autopilot mode provides the ultimate hands-off experience.
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -33,3 +33,17 @@ build/
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/CLAUDE.md
|
||||
/AGENTS.md
|
||||
/src/test/
|
||||
/.claude/agents/backend-architect.md
|
||||
/.dockerignore
|
||||
/Dockerfile
|
||||
/.claude/ralph-loop.local.md
|
||||
/deepgramAPI.md
|
||||
/elevenLabs-websocketAPI.md
|
||||
/elevenlabsAPI.md
|
||||
/Getting Started with Flux.md
|
||||
/voice-optimization-plan.md
|
||||
/docs/websocket-api.md
|
||||
/src/main/resources/static/ws-test.html
|
||||
/.omc/
|
||||
|
||||
7
.omc/ultrawork-state.json
Normal file
7
.omc/ultrawork-state.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"active": true,
|
||||
"started_at": "2026-01-26T13:01:18.447Z",
|
||||
"original_prompt": "刚刚回滚了代码,现在AI陪聊角色评论需要使用KeyboardAiCompanionCommentLikeService添加一个评论点赞接口,用来记录点赞和取消点赞。 ulw",
|
||||
"reinforcement_count": 10,
|
||||
"last_checked_at": "2026-01-27T11:00:42.142Z"
|
||||
}
|
||||
33
AGENTS.md
33
AGENTS.md
@@ -1,33 +0,0 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- Entrypoint `src/main/java/com/yolo/keyborad/MyApplication.java`; feature code organized by layer: `controller` (REST), `service` (business), `mapper` (MyBatis mappers), `model`/`common`/`constant` for DTOs, responses, and constants, plus `config`, `aop`, `annotation`, `Interceptor`, and `utils` for cross-cutting concerns.
|
||||
- Resource configs live in `src/main/resources`: `application.yml` with `application-dev.yml`/`application-prod.yml` profiles, mapper XML files under `mapper/`, and platform keys/certs (Apple, mail, storage). Keep secrets out of commits.
|
||||
- Tests belong in `src/test/java/com/yolo/keyborad/...` mirroring package names; add fixtures alongside tests when needed.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `./mvnw clean install` — full build with tests; requires JDK 17.
|
||||
- `./mvnw test` — run test suite only.
|
||||
- `./mvnw spring-boot:run -Dspring-boot.run.profiles=dev` — start the API with the dev profile (loads `application-dev.yml`).
|
||||
- `./mvnw clean package -DskipTests` — create an artifact when tests are already covered elsewhere.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Java 17, Spring Boot 3.5, MyBatis/MyBatis-Plus; prefer Lombok for boilerplate (`@Data`, `@Builder`) and constructor injection for services.
|
||||
- Use 4-space indentation, lowercase package names, `UpperCamelCase` for classes, `lowerCamelCase` for fields/params.
|
||||
- Controllers end with `*Controller`, services with `*Service`, mapper interfaces with `*Mapper`, and request/response DTOs under `model` or `common` with clear suffixes like `Request`/`Response`.
|
||||
- Keep configuration isolated in `config`; shared constants in `constant`; AOP/logging in `aop`; custom annotations in `annotation`.
|
||||
|
||||
## Testing Guidelines
|
||||
- Use Spring Boot Test + JUnit (via `spring-boot-starter-test`, JUnit 4/5 support) and MockMvc/WebTestClient for HTTP layers when practical.
|
||||
- Name classes `*Test` and align packages with the code under test. Cover service logic, mappers, and controller contracts (status + payload shape).
|
||||
- For data-access tests, use in-memory setups or dedicated test containers and clean up test data.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Follow the existing conventional style seen in history (e.g., `feat(user): add email registration`); keep scope lowercase and concise.
|
||||
- PRs should describe the change, list validation steps/commands run, call out config/profile impacts, and link issues/tasks. Add screenshots or sample requests/responses for API-facing changes when helpful.
|
||||
- Ensure secrets (p8 certificates, Mailgun keys, AWS creds) are never committed; rely on environment variables or local config overrides.
|
||||
|
||||
## Security & Configuration Tips
|
||||
- Activate the intended profile via `SPRING_PROFILES_ACTIVE` or `-Dspring-boot.run.profiles`. Keep `application-dev.yml` local-only; never hardcode production endpoints or credentials.
|
||||
- Validate signing/encryption helpers (`SignInterceptor`, JWT, Apple receipt validation) with representative non-production keys before merging.
|
||||
- Log only necessary context; avoid logging tokens, receipts, or PII.
|
||||
141
README.md
141
README.md
@@ -1,26 +1,123 @@
|
||||
# SpringBoot 项目初始模板
|
||||
# Keyborad Backend
|
||||
|
||||
> Java SpringBoot 项目初始模板,整合了常用框架和示例代码,大家可以在此基础上快速开发自己的项目。
|
||||
基于 Spring Boot 3.5.5 的后端服务,集成了 AI 能力、向量搜索、Apple 登录等功能。
|
||||
|
||||
## 模板功能
|
||||
## 技术栈
|
||||
|
||||
- Spring Boot 2.7.0(贼新)
|
||||
- Spring MVC
|
||||
- MySQL 驱动
|
||||
- MyBatis
|
||||
- MyBatis Plus
|
||||
- Spring Session Redis 分布式登录
|
||||
- Spring AOP
|
||||
- Apache Commons Lang3 工具类
|
||||
- Lombok 注解
|
||||
- Swagger + Knife4j 接口文档
|
||||
- Spring Boot 调试工具和项目处理器
|
||||
- 全局请求响应拦截器(记录日志)
|
||||
- 全局异常处理器
|
||||
- 自定义错误码
|
||||
- 封装通用响应类
|
||||
- 示例用户注册、登录、搜索功能
|
||||
- 示例单元测试类
|
||||
- 示例 SQL(用户表)
|
||||
- **Java 17** + **Spring Boot 3.5.5**
|
||||
- **Spring AI** - LLM 对话和文本嵌入(OpenAI 兼容 API)
|
||||
- **Qdrant** - 向量数据库,支持语义搜索
|
||||
- **PostgreSQL** - 关系型数据库
|
||||
- **MyBatis Plus** - ORM 框架
|
||||
- **Redis** - 会话存储和缓存
|
||||
- **Sa-Token** - 认证授权框架
|
||||
- **Knife4j** - API 文档
|
||||
- **X-File-Storage** - 文件上传(Cloudflare R2)
|
||||
- **MailerSend** - 邮件服务
|
||||
|
||||
访问 localhost:7529/api/doc.html 就能在线调试接口了,不需要前端配合啦~
|
||||
## 核心功能
|
||||
|
||||
### 认证系统
|
||||
- Apple Sign-In JWT 验证
|
||||
- Sa-Token 会话管理
|
||||
- 请求签名校验(防篡改/防重放)
|
||||
|
||||
### AI 能力
|
||||
- LLM 对话(支持流式响应)
|
||||
- 文本嵌入(1536 维向量)
|
||||
- 语义搜索(Qdrant 向量检索)
|
||||
|
||||
### 通用功能
|
||||
- 统一响应格式
|
||||
- 全局异常处理
|
||||
- 国际化支持(i18n)
|
||||
- 请求日志记录
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- JDK 17+
|
||||
- Maven 3.8+
|
||||
- PostgreSQL 14+
|
||||
- Redis 6+
|
||||
|
||||
### 本地运行
|
||||
|
||||
1. 克隆项目
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd keyborad-backend
|
||||
```
|
||||
|
||||
2. 配置数据库和 Redis
|
||||
```yaml
|
||||
# 修改 src/main/resources/application-dev.yml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/keyborad_db
|
||||
username: your_username
|
||||
password: your_password
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
```
|
||||
|
||||
3. 启动应用
|
||||
```bash
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
4. 访问 API 文档
|
||||
```
|
||||
http://localhost:7529/api/doc.html
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/main/java/com/yolo/keyborad/
|
||||
├── controller/ # REST API 端点
|
||||
├── service/ # 业务逻辑层
|
||||
│ └── impl/ # 服务实现
|
||||
├── mapper/ # MyBatis 数据库映射
|
||||
├── model/
|
||||
│ ├── entity/ # 数据库实体
|
||||
│ ├── dto/ # 请求数据传输对象
|
||||
│ └── vo/ # 响应视图对象
|
||||
├── config/ # Spring 配置类
|
||||
├── aop/ # AOP 拦截器
|
||||
├── Interceptor/ # 请求拦截器
|
||||
├── filter/ # Servlet 过滤器
|
||||
├── exception/ # 异常处理
|
||||
├── common/ # 通用工具类
|
||||
└── utils/ # 工具类
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|-------|------|-------|
|
||||
| `server.port` | 服务端口 | 7529 |
|
||||
| `server.servlet.context-path` | 上下文路径 | /api |
|
||||
| `spring.profiles.active` | 激活配置文件 | dev |
|
||||
|
||||
## API 认证
|
||||
|
||||
### Sa-Token 认证
|
||||
需要在请求头中携带 `satoken` 字段。
|
||||
|
||||
### 请求签名
|
||||
部分接口需要签名校验,请求头需包含:
|
||||
- `X-App-Id` - 应用 ID
|
||||
- `X-Timestamp` - 时间戳
|
||||
- `X-Nonce` - 随机数
|
||||
- `X-Sign` - 签名
|
||||
|
||||
## 开发指南
|
||||
|
||||
详细的开发指南请参考 [CLAUDE.md](./CLAUDE.md)。
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
316
mvnw
vendored
316
mvnw
vendored
@@ -1,316 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven Start Up Batch script
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# M2_HOME - location of maven2's installed home dir
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
||||
else
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`\\unset -f command; \\command -v java`"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
echo "${basedir}"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=`find_maven_basedir "$(pwd)"`
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
||||
fi
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
||||
fi
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
else
|
||||
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
fi
|
||||
while IFS="=" read key value; do
|
||||
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
|
||||
esac
|
||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Downloading from: $jarUrl"
|
||||
fi
|
||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if $cygwin; then
|
||||
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl -o "$wrapperJarPath" "$jarUrl" -f
|
||||
else
|
||||
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaClass=`cygpath --path --windows "$javaClass"`
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
# Compiling the Java class
|
||||
("$JAVA_HOME/bin/javac" "$javaClass")
|
||||
fi
|
||||
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
# Running the downloader
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Running MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
fi
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
188
mvnw.cmd
vendored
188
mvnw.cmd
vendored
@@ -1,188 +0,0 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM https://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Maven Start Up Batch script
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM M2_HOME - location of maven2's installed home dir
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %DOWNLOAD_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
||||
30
pom.xml
30
pom.xml
@@ -55,6 +55,13 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.nacos</groupId>
|
||||
<artifactId>nacos-client</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- qdrant向量数据库 sdk -->
|
||||
<dependency>
|
||||
<groupId>io.qdrant</groupId>
|
||||
@@ -101,7 +108,7 @@
|
||||
<dependency>
|
||||
<groupId>com.apple.itunes.storekit</groupId>
|
||||
<artifactId>app-store-server-library</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>4.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- x-file-storage -->
|
||||
@@ -273,6 +280,27 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.38</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
|
||||
BIN
src/main/.DS_Store
vendored
Normal file
BIN
src/main/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
package com.yolo.keyborad.Interceptor;
|
||||
package com.yolo.keyborad.interceptor;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yolo.keyborad.utils.SignUtils;
|
||||
|
||||
@@ -28,6 +28,12 @@ public enum ErrorCode {
|
||||
CHAT_CHARACTER_NOT_FOUND(40008, "键盘人设不存在"),
|
||||
CHAT_MESSAGE_TOO_LONG(40009, "聊天消息过长,最大支持1000字符"),
|
||||
CHAT_SAVE_DATA_EMPTY(40010, "保存数据不能为空"),
|
||||
COMPANION_MESSAGE_EMPTY(40011, "消息内容不能为空"),
|
||||
COMPANION_ID_EMPTY(40012, "AI陪聊角色ID不能为空"),
|
||||
COMPANION_NOT_FOUND(40019, "AI陪聊角色不存在"),
|
||||
COMMENT_CONTENT_EMPTY(40013, "评论内容不能为空"),
|
||||
COMMENT_NOT_FOUND(40014, "评论不存在"),
|
||||
COMMENT_ID_EMPTY(40015, "评论ID不能为空"),
|
||||
TOKEN_NOT_FOUND(40102, "未能读取到有效用户令牌"),
|
||||
TOKEN_INVALID(40103, "令牌无效"),
|
||||
TOKEN_TIMEOUT(40104, "令牌已过期"),
|
||||
@@ -50,7 +56,30 @@ public enum ErrorCode {
|
||||
INSUFFICIENT_BALANCE(50013, "余额不足"),
|
||||
THEME_NOT_FOUND(40410, "主题不存在"),
|
||||
THEME_ALREADY_PURCHASED(50014, "主题已购买"),
|
||||
THEME_NOT_AVAILABLE(50015, "主题不可购买");
|
||||
THEME_NOT_AVAILABLE(50015, "主题不可购买"),
|
||||
RECEIPT_INVALID(50016, "收据无效"),
|
||||
UPDATE_USER_VIP_STATUS_ERROR(50017, "更新用户VIP状态失败"),
|
||||
PRODUCT_QUOTA_NOT_SET(50018, "商品额度未配置"),
|
||||
LACK_ORIGIN_TRANSACTION_ID_ERROR(50019, "缺少原始交易id"),
|
||||
UNKNOWN_PRODUCT_TYPE(50020, "未知商品类型"),
|
||||
PRODUCT_NOT_FOUND(50021, "商品不存在"),
|
||||
NO_QUOTA_AND_NOT_VIP(50022, "免费次数已用完,请开通VIP"),
|
||||
INVITE_CODE_NOT_FOUND(50023, "邀请码不存在"),
|
||||
INVITE_CODE_INVALID(50024, "邀请码无效"),
|
||||
INVITE_CODE_EXPIRED(50025, "邀请码已过期"),
|
||||
INVITE_CODE_USED_UP(50026, "邀请码使用次数已达上限"),
|
||||
INVITE_CODE_ALREADY_BOUND(50028, "您已绑定过邀请码,无法重复绑定"),
|
||||
INVITE_CODE_CANNOT_BIND_SELF(50029, "不能绑定自己的邀请码"),
|
||||
RECEIPT_ALREADY_PROCESSED(50027, "收据已处理"),
|
||||
VIP_TRIAL_LIMIT_REACHED(50030, "今日体验次数已达上限,请开通会员"),
|
||||
AUDIO_FILE_EMPTY(40016, "音频文件不能为空"),
|
||||
AUDIO_FILE_TOO_LARGE(40017, "音频文件过大"),
|
||||
AUDIO_FORMAT_NOT_SUPPORTED(40018, "音频格式不支持"),
|
||||
STT_SERVICE_ERROR(50031, "语音转文字服务异常"),
|
||||
REPORT_TYPE_INVALID(40020, "举报类型无效"),
|
||||
REPORT_COMPANION_ID_EMPTY(40021, "被举报的AI角色ID不能为空"),
|
||||
REPORT_TYPE_EMPTY(40022, "举报类型不能为空");
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
|
||||
60
src/main/java/com/yolo/keyborad/config/AppConfig.java
Normal file
60
src/main/java/com/yolo/keyborad/config/AppConfig.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package com.yolo.keyborad.config;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/16 21:18
|
||||
*/
|
||||
@Data
|
||||
public class AppConfig {
|
||||
|
||||
private UserRegisterProperties userRegisterProperties = new UserRegisterProperties();
|
||||
|
||||
private QdrantConfig qdrantConfig = new QdrantConfig();
|
||||
|
||||
private LLmConfig llmConfig = new LLmConfig();
|
||||
|
||||
private inviteConfig inviteConfig = new inviteConfig();
|
||||
|
||||
@Data
|
||||
public static class UserRegisterProperties {
|
||||
|
||||
//新用户注册时的免费使用次数
|
||||
private Integer freeTrialQuota = 3;
|
||||
|
||||
//Vip用户每天能免费聊天次数
|
||||
private Integer vipFreeTrialTalk = 3;
|
||||
|
||||
//新用户注册时的奖励余额
|
||||
private BigDecimal rewardBalance = BigDecimal.valueOf(0);
|
||||
}
|
||||
|
||||
|
||||
@Data
|
||||
public static class QdrantConfig {
|
||||
//向量搜索时的返回数量限制
|
||||
private Integer vectorSearchLimit = 1;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LLmConfig {
|
||||
//LLM系统提示语
|
||||
private String systemPrompt = """
|
||||
Format rules:
|
||||
- Return EXACTLY 3 replies.
|
||||
- Use "<SPLIT>" as the separator.
|
||||
- reply1<SPLIT>reply2<SPLIT>reply3
|
||||
""";
|
||||
|
||||
//聊天消息最大长度
|
||||
private Integer maxMessageLength = 1000;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class inviteConfig {
|
||||
private String h5Link = "";
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,7 @@ public class AppleAppStoreConfig {
|
||||
public AppStoreServerAPIClient appStoreServerAPIClient() throws Exception {
|
||||
// 加载私钥文件
|
||||
Resource keyResource = resourceLoader.getResource(properties.getPrivateKeyPath());
|
||||
Path keyPath = keyResource.getFile().toPath();
|
||||
String encodedKey = Files.readString(keyPath);
|
||||
String encodedKey = new String(keyResource.getInputStream().readAllBytes());
|
||||
|
||||
// 获取环境配置(沙盒或生产)
|
||||
Environment env = Environment.valueOf(properties.getEnvironment());
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.yolo.keyborad.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Deepgram STT 配置
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "deepgram")
|
||||
public class DeepgramProperties {
|
||||
|
||||
/** API Key */
|
||||
private String apiKey;
|
||||
|
||||
/** 基础 URL */
|
||||
private String baseUrl = "https://api.deepgram.com/v1";
|
||||
|
||||
/** 模型 ID */
|
||||
private String model = "nova-2";
|
||||
|
||||
/** 默认语言 */
|
||||
private String language = "en";
|
||||
|
||||
/** 智能格式化 */
|
||||
private Boolean smartFormat = true;
|
||||
|
||||
/** 添加标点符号 */
|
||||
private Boolean punctuate = true;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.yolo.keyborad.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* ElevenLabs TTS 配置
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "elevenlabs")
|
||||
public class ElevenLabsProperties {
|
||||
|
||||
/**
|
||||
* API Key
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 基础 URL
|
||||
*/
|
||||
private String baseUrl = "https://api.elevenlabs.io/v1";
|
||||
|
||||
/**
|
||||
* 默认语音 ID
|
||||
*/
|
||||
private String voiceId;
|
||||
|
||||
/**
|
||||
* 模型 ID
|
||||
*/
|
||||
private String modelId = "eleven_multilingual_v2";
|
||||
|
||||
/**
|
||||
* 输出格式
|
||||
*/
|
||||
private String outputFormat = "mp3_44100_128";
|
||||
|
||||
/**
|
||||
* 稳定性 (0-1)
|
||||
*/
|
||||
private Double stability = 0.5;
|
||||
|
||||
/**
|
||||
* 相似度增强 (0-1)
|
||||
*/
|
||||
private Double similarityBoost = 0.75;
|
||||
|
||||
/**
|
||||
* 风格 (0-1)
|
||||
*/
|
||||
private Double style = 0.0;
|
||||
|
||||
/**
|
||||
* 语速 (0.7-1.2)
|
||||
*/
|
||||
private Double speed = 1.0;
|
||||
|
||||
/**
|
||||
* 使用说话人增强
|
||||
*/
|
||||
private Boolean useSpeakerBoost = true;
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import org.springframework.ai.retry.RetryUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/*
|
||||
@@ -32,6 +35,7 @@ public class LLMConfig {
|
||||
public OpenAiApi openAiApi() {
|
||||
return OpenAiApi.builder()
|
||||
.apiKey(apiKey)
|
||||
.headers(MultiValueMap.fromSingleValue(Map.of("X-Title", "key of love")))
|
||||
.baseUrl(baseUrl)
|
||||
.build();
|
||||
}
|
||||
@@ -53,7 +57,7 @@ public class LLMConfig {
|
||||
this.openAiApi(),
|
||||
MetadataMode.EMBED,
|
||||
OpenAiEmbeddingOptions.builder()
|
||||
.model("qwen/qwen3-embedding-4b")
|
||||
.model("text-embedding-v4")
|
||||
.dimensions(1536)
|
||||
.user("user-6")
|
||||
.build(),
|
||||
|
||||
@@ -25,7 +25,7 @@ public class MyBatisPlusConfig {
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.yolo.keyborad.config;
|
||||
|
||||
import com.alibaba.nacos.api.NacosFactory;
|
||||
import com.alibaba.nacos.api.config.ConfigService;
|
||||
import com.alibaba.nacos.api.config.listener.Listener;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class NacosAppConfigCenter {
|
||||
|
||||
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory())
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
@Bean
|
||||
public ConfigService nacosConfigService(
|
||||
@Value("${nacos.config.server-addr}") String serverAddr
|
||||
) throws NacosException {
|
||||
Properties p = new Properties();
|
||||
p.put("serverAddr", serverAddr);
|
||||
return NacosFactory.createConfigService(p);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DynamicAppConfig dynamicAppConfig(
|
||||
ConfigService configService,
|
||||
@Value("${nacos.config.group}") String group,
|
||||
@Value("${nacos.config.data-id}") String dataId
|
||||
) throws Exception {
|
||||
|
||||
DynamicAppConfig holder = new DynamicAppConfig();
|
||||
|
||||
// 启动先拉一次
|
||||
String content = configService.getConfig(dataId, group, 3000);
|
||||
if (content != null && !content.isBlank()) {
|
||||
holder.ref.set(parse(content));
|
||||
log.info("Loaded nacos config: dataId={}, group={}", dataId, group);
|
||||
} else {
|
||||
log.warn("Empty nacos config: dataId={}, group={}", dataId, group);
|
||||
}
|
||||
|
||||
// 监听热更新
|
||||
configService.addListener(dataId, group, new Listener() {
|
||||
@Override public Executor getExecutor() { return null; }
|
||||
@Override public void receiveConfigInfo(String configInfo) {
|
||||
try {
|
||||
AppConfig newCfg = parse(configInfo);
|
||||
holder.ref.set(newCfg);
|
||||
log.info("Refreshed nacos config: dataId={}, group={}", dataId, group);
|
||||
log.info("New config: {}", newCfg.toString());
|
||||
} catch (Exception e) {
|
||||
// 解析失败不覆盖旧配置
|
||||
log.error("Failed to refresh nacos config: dataId={}, keep old config.", dataId, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return holder;
|
||||
}
|
||||
|
||||
private AppConfig parse(String yaml) throws Exception {
|
||||
if (yaml == null || yaml.isBlank()) return new AppConfig();
|
||||
return yamlMapper.readValue(yaml, AppConfig.class);
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class DynamicAppConfig {
|
||||
private final AtomicReference<AppConfig> ref = new AtomicReference<>(new AppConfig());
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class RedisConfig {
|
||||
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
StringRedisTemplate template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
|
||||
// 设置key序列化方式
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
// 设置value序列化方式
|
||||
@@ -33,7 +33,30 @@ public class RedisConfig {
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
// 设置hash value序列化方式
|
||||
template.setHashValueSerializer(new StringRedisSerializer());
|
||||
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置对象序列化的RedisTemplate
|
||||
* @param connectionFactory Redis连接工厂
|
||||
* @return RedisTemplate实例
|
||||
*/
|
||||
@Bean("objectRedisTemplate")
|
||||
public org.springframework.data.redis.core.RedisTemplate<String, Object> objectRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
org.springframework.data.redis.core.RedisTemplate<String, Object> template = new org.springframework.data.redis.core.RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 设置key序列化方式
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
// 设置value序列化方式(使用JSON序列化)
|
||||
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
// 设置hash key序列化方式
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
// 设置hash value序列化方式
|
||||
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
25
src/main/java/com/yolo/keyborad/config/RestClientConfig.java
Normal file
25
src/main/java/com/yolo/keyborad/config/RestClientConfig.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.yolo.keyborad.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* RestClient 配置类
|
||||
* 提供连接池复用,优化 HTTP 请求性能
|
||||
*/
|
||||
@Configuration
|
||||
public class RestClientConfig {
|
||||
|
||||
@Bean
|
||||
public RestClient restClient() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(30000);
|
||||
factory.setReadTimeout(60000);
|
||||
|
||||
return RestClient.builder()
|
||||
.requestFactory(factory)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.yolo.keyborad.Interceptor.SignInterceptor;
|
||||
import com.yolo.keyborad.interceptor.SignInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -64,7 +64,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
"/demo/embed",
|
||||
"/demo/testSaveEmbed",
|
||||
"/demo/testSearch",
|
||||
"/demo/tsetSearchText",
|
||||
"/demo/testSearchText",
|
||||
"/file/upload",
|
||||
"/user/logout",
|
||||
"/tag/list",
|
||||
@@ -73,15 +73,16 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
"/character/listByUser",
|
||||
"/user/detail",
|
||||
"/user/register",
|
||||
"/user/updateInfo",
|
||||
"/character/updateUserCharacterSort",
|
||||
"/character/delUserCharacter",
|
||||
"/user/sendVerifyMail",
|
||||
"/user/verifyMailCode",
|
||||
"/character/listWithNotLogin",
|
||||
"/character/listByTagWithNotLogin",
|
||||
"/character/listByTag",
|
||||
"/character/detailWithNotLogin",
|
||||
"/character/addUserCharacter",
|
||||
"/api/apple/validate-receipt",
|
||||
"/character/list",
|
||||
"/user/resetPassWord",
|
||||
"/chat/talk",
|
||||
@@ -93,7 +94,30 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
"/themes/purchase/list",
|
||||
"/themes/detail",
|
||||
"/themes/recommended",
|
||||
"/user-themes/batch-delete"
|
||||
"/themes/search",
|
||||
"/user-themes/batch-delete",
|
||||
"/products/listByType",
|
||||
"/products/detail",
|
||||
"/products/inApp/list",
|
||||
"/products/subscription/list",
|
||||
"/purchase/handle",
|
||||
"/apple/notification",
|
||||
"/apple/receipt",
|
||||
"/apple/validate-receipt",
|
||||
"/user/inviteCode",
|
||||
"/user/bindInviteCode",
|
||||
"/themes/listAllStyles",
|
||||
"/wallet/transactions",
|
||||
"/themes/restore",
|
||||
"/chat/message",
|
||||
"/chat/voice",
|
||||
"/chat/audio/*",
|
||||
"/ai-companion/page",
|
||||
"/chat/history",
|
||||
"/ai-companion/comment/add",
|
||||
"/speech/transcribe",
|
||||
"/ai-companion/comment/page",
|
||||
"/ai-companion/liked"
|
||||
};
|
||||
}
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.dto.comment.CommentAddReq;
|
||||
import com.yolo.keyborad.model.dto.comment.CommentLikeReq;
|
||||
import com.yolo.keyborad.model.dto.comment.CommentPageReq;
|
||||
import com.yolo.keyborad.model.vo.CommentVO;
|
||||
import com.yolo.keyborad.service.KeyboardAiCompanionCommentService;
|
||||
import com.yolo.keyborad.service.KeyboardAiCompanionCommentLikeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@RestController
|
||||
@Slf4j
|
||||
@RequestMapping("/ai-companion/comment")
|
||||
@Tag(name = "AI陪聊角色评论", description = "AI陪聊角色评论管理接口")
|
||||
public class AiCompanionCommentController {
|
||||
|
||||
@Resource
|
||||
private KeyboardAiCompanionCommentService commentService;
|
||||
|
||||
@Resource
|
||||
private KeyboardAiCompanionCommentLikeService commentLikeService;
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "发表评论", description = "用户对AI陪聊角色发表评论")
|
||||
public BaseResponse<Long> addComment(@RequestBody CommentAddReq req) {
|
||||
if (req.getCompanionId() == null) {
|
||||
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||
}
|
||||
if (StrUtil.isBlank(req.getContent())) {
|
||||
throw new BusinessException(ErrorCode.COMMENT_CONTENT_EMPTY);
|
||||
}
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Long commentId = commentService.addComment(userId, req.getCompanionId(), req.getContent(),
|
||||
req.getParentId(), req.getRootId());
|
||||
return ResultUtils.success(commentId);
|
||||
}
|
||||
|
||||
@PostMapping("/page")
|
||||
@Operation(summary = "分页查询评论", description = "分页查询AI陪聊角色的评论列表,包含当前用户是否已点赞状态")
|
||||
public BaseResponse<IPage<CommentVO>> pageComments(@RequestBody CommentPageReq req) {
|
||||
if (req.getCompanionId() == null) {
|
||||
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||
}
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
IPage<CommentVO> result = commentService.pageCommentsWithLikeStatus(userId, req.getCompanionId(),
|
||||
req.getPageNum(), req.getPageSize());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/like")
|
||||
@Operation(summary = "点赞/取消点赞", description = "对评论进行点赞或取消点赞操作,返回true表示点赞成功,false表示取消点赞成功")
|
||||
public BaseResponse<Boolean> toggleLike(@RequestBody CommentLikeReq req) {
|
||||
if (req.getCommentId() == null) {
|
||||
throw new BusinessException(ErrorCode.COMMENT_ID_EMPTY);
|
||||
}
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
boolean result = commentLikeService.toggleLike(userId, req.getCommentId());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.dto.PageDTO;
|
||||
import com.yolo.keyborad.model.dto.companion.CompanionLikeReq;
|
||||
import com.yolo.keyborad.model.dto.companion.CompanionReportReq;
|
||||
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
||||
import com.yolo.keyborad.service.KeyboardAiCompanionLikeService;
|
||||
import com.yolo.keyborad.service.KeyboardAiCompanionReportService;
|
||||
import com.yolo.keyborad.service.KeyboardAiCompanionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@RestController
|
||||
@Slf4j
|
||||
@RequestMapping("/ai-companion")
|
||||
@Tag(name = "AI陪聊角色", description = "AI陪聊角色管理接口")
|
||||
public class AiCompanionController {
|
||||
|
||||
@Resource
|
||||
private KeyboardAiCompanionService aiCompanionService;
|
||||
|
||||
@Resource
|
||||
private KeyboardAiCompanionLikeService aiCompanionLikeService;
|
||||
|
||||
@Resource
|
||||
private KeyboardAiCompanionReportService aiCompanionReportService;
|
||||
|
||||
@PostMapping("/page")
|
||||
@Operation(summary = "分页查询AI陪聊角色", description = "分页查询已上线的AI陪聊角色列表,包含点赞数、评论数和当前用户点赞状态")
|
||||
public BaseResponse<IPage<AiCompanionVO>> pageList(@RequestBody PageDTO pageDTO) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
IPage<AiCompanionVO> result = aiCompanionService.pageListWithLikeStatus(userId, pageDTO.getPageNum(), pageDTO.getPageSize());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/like")
|
||||
@Operation(summary = "点赞/取消点赞AI角色", description = "对AI角色进行点赞或取消点赞操作,返回true表示点赞成功,false表示取消点赞成功")
|
||||
public BaseResponse<Boolean> toggleLike(@RequestBody CompanionLikeReq req) {
|
||||
if (req.getCompanionId() == null) {
|
||||
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||
}
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
boolean result = aiCompanionLikeService.toggleLike(userId, req.getCompanionId());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/liked")
|
||||
@Operation(summary = "获取当前用户点赞过的AI角色列表", description = "查询当前用户点赞过的所有AI角色,返回角色详细信息")
|
||||
public BaseResponse<List<AiCompanionVO>> getLikedCompanions() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
List<AiCompanionVO> result = aiCompanionService.getLikedCompanions(userId);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/chatted")
|
||||
@Operation(summary = "获取当前用户聊过天的AI角色列表", description = "查询当前用户聊过天的所有AI角色,返回角色详细信息")
|
||||
public BaseResponse<List<AiCompanionVO>> getChattedCompanions() {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
List<AiCompanionVO> result = aiCompanionService.getChattedCompanions(userId);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{companionId}")
|
||||
@Operation(summary = "根据ID获取AI角色详情", description = "根据AI角色ID查询角色详细信息,包含点赞数、评论数和当前用户点赞状态")
|
||||
public BaseResponse<AiCompanionVO> getCompanionById(@PathVariable Long companionId) {
|
||||
if (companionId == null) {
|
||||
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||
}
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
AiCompanionVO result = aiCompanionService.getCompanionById(userId, companionId);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/report")
|
||||
@Operation(summary = "举报AI角色", description = "举报AI角色,支持多种举报类型(可多选):1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他")
|
||||
public BaseResponse<Long> reportCompanion(@RequestBody CompanionReportReq req) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
Long reportId = aiCompanionReportService.reportCompanion(userId, req);
|
||||
return ResultUtils.success(reportId);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,88 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
|
||||
import com.yolo.keyborad.service.ApplePurchaseService;
|
||||
import com.yolo.keyborad.service.AppleReceiptService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/apple")
|
||||
@RequestMapping("/apple")
|
||||
@Slf4j
|
||||
public class AppleReceiptController {
|
||||
|
||||
private final AppleReceiptService appleReceiptService;
|
||||
private final ApplePurchaseService applePurchaseService;
|
||||
|
||||
public AppleReceiptController(AppleReceiptService appleReceiptService) {
|
||||
public AppleReceiptController(AppleReceiptService appleReceiptService,
|
||||
ApplePurchaseService applePurchaseService) {
|
||||
this.appleReceiptService = appleReceiptService;
|
||||
this.applePurchaseService = applePurchaseService;
|
||||
}
|
||||
|
||||
@PostMapping("/receipt")
|
||||
public AppleReceiptValidationResult validateReceipt(@RequestBody Map<String, String> body) {
|
||||
if (body == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "body 不能为空");
|
||||
}
|
||||
String receipt = body.get("receipt");
|
||||
if (receipt == null || receipt.isBlank()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "receipt 不能为空");
|
||||
}
|
||||
return appleReceiptService.validateReceipt(receipt);
|
||||
}
|
||||
|
||||
@PostMapping("/validate-receipt")
|
||||
public AppleReceiptValidationResult validateReceipt(@RequestBody Map<String, String> body) {
|
||||
String receipt = body.get("receipt");
|
||||
return appleReceiptService.validateReceipt(receipt);
|
||||
public BaseResponse<Boolean> handlePurchase(@RequestBody Map<String, String> body) {
|
||||
if (body == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "body 不能为空");
|
||||
}
|
||||
String signedPayload = body.get("signedPayload");
|
||||
if (signedPayload == null || signedPayload.isBlank()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "signedPayload 不能为空");
|
||||
}
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
AppleReceiptValidationResult validationResult = appleReceiptService.validateReceipt(signedPayload);
|
||||
applePurchaseService.processPurchase(userId, validationResult);
|
||||
return ResultUtils.success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 接收 Apple 服务器通知
|
||||
* 处理来自 Apple 的服务器到服务器通知,包括订阅续订、退款等事件
|
||||
* 所有验证和处理逻辑都委托给 service 层
|
||||
*
|
||||
* @param body 请求体,包含 signedPayload 字段
|
||||
* @return 处理结果
|
||||
* @throws BusinessException 当 signedPayload 为空时抛出
|
||||
*/
|
||||
@PostMapping("/notification")
|
||||
public BaseResponse<Boolean> receiveNotification(@RequestBody Map<String, String> body) {
|
||||
// 参数校验
|
||||
if (body == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "body 不能为空");
|
||||
}
|
||||
|
||||
String signedPayload = body.get("signedPayload");
|
||||
if (signedPayload == null || signedPayload.isBlank()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "signedPayload 不能为空");
|
||||
}
|
||||
|
||||
// 委托给 service 层处理所有通知逻辑
|
||||
appleReceiptService.processNotification(signedPayload);
|
||||
|
||||
return ResultUtils.success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,8 +49,7 @@ public class CharacterController {
|
||||
@GetMapping("/detail")
|
||||
@Operation(summary = "人设详情", description = "人设详情接口")
|
||||
public BaseResponse<KeyboardCharacterRespVO> detail(@RequestParam("id") Long id) {
|
||||
KeyboardCharacter character = characterService.getById(id);
|
||||
return ResultUtils.success(BeanUtil.copyProperties(character, KeyboardCharacterRespVO.class));
|
||||
return ResultUtils.success(characterService.getDetailById(id));
|
||||
}
|
||||
|
||||
@GetMapping("/listByTag")
|
||||
|
||||
@@ -3,33 +3,37 @@ package com.yolo.keyborad.controller;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.mapper.QdrantPayloadMapper;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatHistoryPageReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatMessageReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatSaveReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||
import com.yolo.keyborad.service.KeyboardCharacterService;
|
||||
import com.yolo.keyborad.model.dto.chat.SessionResetReq;
|
||||
import com.yolo.keyborad.model.vo.AudioTaskVO;
|
||||
import com.yolo.keyborad.model.vo.ChatMessageHistoryVO;
|
||||
import com.yolo.keyborad.model.vo.ChatMessageVO;
|
||||
import com.yolo.keyborad.model.vo.ChatSessionVO;
|
||||
import com.yolo.keyborad.model.vo.ChatVoiceVO;
|
||||
import com.yolo.keyborad.service.ChatService;
|
||||
import com.yolo.keyborad.service.KeyboardAiChatMessageService;
|
||||
import com.yolo.keyborad.service.KeyboardAiChatSessionService;
|
||||
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
||||
import io.qdrant.client.grpc.JsonWithInt;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingModel;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/*
|
||||
@@ -42,12 +46,6 @@ import java.util.Map;
|
||||
@Tag(name = "聊天", description = "聊天接口")
|
||||
public class ChatController {
|
||||
|
||||
// 最大消息长度限制
|
||||
private static final int MAX_MESSAGE_LENGTH = 1000;
|
||||
|
||||
@Resource
|
||||
private ChatClient client;
|
||||
|
||||
@Resource
|
||||
private OpenAiEmbeddingModel embeddingModel;
|
||||
|
||||
@@ -55,109 +53,47 @@ public class ChatController {
|
||||
private QdrantVectorService qdrantVectorService;
|
||||
|
||||
@Resource
|
||||
private KeyboardCharacterService keyboardCharacterService;
|
||||
private ChatService chatService;
|
||||
|
||||
@Resource
|
||||
private KeyboardAiChatMessageService aiChatMessageService;
|
||||
|
||||
@Resource
|
||||
private KeyboardAiChatSessionService aiChatSessionService;
|
||||
|
||||
|
||||
@PostMapping("/message")
|
||||
@Operation(summary = "同步对话", description = "发送消息给大模型,同步返回 AI 响应,异步生成音频")
|
||||
public BaseResponse<ChatMessageVO> message(@RequestBody ChatMessageReq req ) {
|
||||
if (StrUtil.isBlank(req.getContent())) {
|
||||
throw new BusinessException(ErrorCode.COMPANION_MESSAGE_EMPTY);
|
||||
}
|
||||
if (req.getCompanionId() == null) {
|
||||
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||
}
|
||||
|
||||
String userId = StpUtil.getLoginIdAsString();
|
||||
ChatMessageVO result = chatService.message(req.getContent(), userId, req.getCompanionId());
|
||||
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/audio/{audioId}")
|
||||
@Operation(summary = "查询音频状态", description = "根据音频 ID 查询音频生成状态和 URL")
|
||||
public BaseResponse<AudioTaskVO> getAudioTask(@PathVariable("audioId") String audioId) {
|
||||
if (StrUtil.isBlank(audioId)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "音频 ID 不能为空");
|
||||
}
|
||||
|
||||
AudioTaskVO result = chatService.getAudioTask(audioId);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/talk")
|
||||
@Operation(summary = "聊天润色接口", description = "聊天润色接口")
|
||||
public Flux<ServerSentEvent<ChatStreamMessage>> talk(@RequestBody ChatReq chatReq){
|
||||
// 1. 参数校验
|
||||
if (chatReq == null) {
|
||||
log.error("聊天请求参数为空");
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
|
||||
if (chatReq.getCharacterId() == null) {
|
||||
log.error("键盘人设ID为空");
|
||||
throw new BusinessException(ErrorCode.CHAT_CHARACTER_ID_EMPTY);
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(chatReq.getMessage())) {
|
||||
log.error("聊天消息为空");
|
||||
throw new BusinessException(ErrorCode.CHAT_MESSAGE_EMPTY);
|
||||
}
|
||||
|
||||
if (chatReq.getMessage().length() > MAX_MESSAGE_LENGTH) {
|
||||
log.error("聊天消息过长,长度: {}", chatReq.getMessage().length());
|
||||
throw new BusinessException(ErrorCode.CHAT_MESSAGE_TOO_LONG);
|
||||
}
|
||||
|
||||
// 2. 验证键盘人设是否存在
|
||||
KeyboardCharacter character = keyboardCharacterService.getById(chatReq.getCharacterId());
|
||||
if (character == null) {
|
||||
log.error("键盘人设不存在,ID: {}", chatReq.getCharacterId());
|
||||
throw new BusinessException(ErrorCode.CHAT_CHARACTER_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 3. LLM 流式输出
|
||||
Flux<ChatStreamMessage> llmFlux = client
|
||||
.prompt(character.getPrompt())
|
||||
.system("""
|
||||
Format rules:
|
||||
- Return EXACTLY 3 replies.
|
||||
- Use "<SPLIT>" as the separator.
|
||||
- reply1<SPLIT>reply2<SPLIT>reply3
|
||||
""")
|
||||
.user(chatReq.getMessage())
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.user(StpUtil.getLoginIdAsString())
|
||||
.build())
|
||||
.stream()
|
||||
.content()
|
||||
.concatMap(chunk -> {
|
||||
// 拆成单字符
|
||||
List<String> chars = chunk.codePoints()
|
||||
.mapToObj(cp -> new String(Character.toChars(cp)))
|
||||
.toList();
|
||||
|
||||
// 按 3 个字符批量发送
|
||||
List<String> batched = new ArrayList<>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String ch : chars) {
|
||||
sb.append(ch);
|
||||
if (sb.length() >= 3) {
|
||||
batched.add(sb.toString());
|
||||
sb.setLength(0);
|
||||
}
|
||||
}
|
||||
if (!sb.isEmpty()) {
|
||||
batched.add(sb.toString());
|
||||
}
|
||||
|
||||
return Flux.fromIterable(batched)
|
||||
.map(s -> new ChatStreamMessage("llm_chunk", s));
|
||||
})
|
||||
.doOnError(error -> log.error("LLM调用失败", error))
|
||||
.onErrorResume(error ->
|
||||
Flux.just(new ChatStreamMessage("error", "LLM服务暂时不可用,请稍后重试"))
|
||||
);
|
||||
|
||||
// 4. 向量搜索Flux(一次性发送搜索结果)
|
||||
Flux<ChatStreamMessage> searchFlux = Mono
|
||||
.fromCallable(() -> qdrantVectorService.searchText(chatReq.getMessage()))
|
||||
.subscribeOn(Schedulers.boundedElastic()) // 避免阻塞 event-loop
|
||||
.map(list -> new ChatStreamMessage("search_result", list))
|
||||
.doOnError(error -> log.error("向量搜索失败", error))
|
||||
.onErrorResume(error ->
|
||||
Mono.just(new ChatStreamMessage("search_result", new ArrayList<>()))
|
||||
)
|
||||
.flux();
|
||||
|
||||
// 5. 结束标记
|
||||
Flux<ChatStreamMessage> doneFlux =
|
||||
Flux.just(new ChatStreamMessage("done", null));
|
||||
|
||||
// 6. 合并所有Flux
|
||||
Flux<ChatStreamMessage> merged =
|
||||
Flux.merge(llmFlux, searchFlux)
|
||||
.concatWith(doneFlux);
|
||||
|
||||
// 7. SSE 包装
|
||||
return merged.map(msg ->
|
||||
ServerSentEvent.builder(msg)
|
||||
.event(msg.getType())
|
||||
.build()
|
||||
);
|
||||
return chatService.talk(chatReq);
|
||||
}
|
||||
|
||||
|
||||
@@ -188,4 +124,37 @@ public class ChatController {
|
||||
log.info("聊天嵌入保存成功,用户ID: {}, 文本长度: {}", chatSaveReq.getUserId(), chatSaveReq.getUserText().length());
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/history")
|
||||
@Operation(summary = "分页查询聊天记录", description = "分页查询用户与AI陪聊角色的聊天记录")
|
||||
public BaseResponse<IPage<ChatMessageHistoryVO>> pageHistory(@RequestBody ChatHistoryPageReq req) {
|
||||
if (req.getCompanionId() == null) {
|
||||
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||
}
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
IPage<ChatMessageHistoryVO> result = aiChatMessageService.pageHistory(
|
||||
userId, req.getCompanionId(), req.getPageNum(), req.getPageSize());
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/session/reset")
|
||||
@Operation(summary = "重置会话", description = "重置与AI角色的聊天会话,将当前会话设为不活跃并创建新会话,后续聊天记录将绑定到新会话")
|
||||
public BaseResponse<ChatSessionVO> resetSession(@RequestBody SessionResetReq req) {
|
||||
if (req.getCompanionId() == null) {
|
||||
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||
}
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
var newSession = aiChatSessionService.resetSession(userId, req.getCompanionId());
|
||||
|
||||
ChatSessionVO vo = ChatSessionVO.builder()
|
||||
.sessionId(newSession.getId())
|
||||
.companionId(newSession.getCompanionId())
|
||||
.resetVersion(newSession.getResetVersion())
|
||||
.createdAt(newSession.getCreatedAt())
|
||||
.build();
|
||||
|
||||
return ResultUtils.success(vo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
|
||||
import com.yolo.keyborad.model.dto.EmbedSaveReq;
|
||||
import com.yolo.keyborad.model.dto.IosPayVerifyReq;
|
||||
import com.yolo.keyborad.model.dto.SearchEmbedReq;
|
||||
import com.yolo.keyborad.model.dto.TextSearchReq;
|
||||
import com.yolo.keyborad.model.vo.QdrantSearchItem;
|
||||
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.embedding.Embedding;
|
||||
import org.springframework.ai.embedding.EmbeddingResponse;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingModel;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/10/28 20:42
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/demo")
|
||||
@Slf4j
|
||||
@CrossOrigin
|
||||
@Tag(name = "测试控制器", description = "测试控制器")
|
||||
public class DemoController {
|
||||
|
||||
@Resource
|
||||
private ChatClient client;
|
||||
|
||||
@Resource
|
||||
private OpenAiEmbeddingModel embeddingModel;
|
||||
|
||||
@Resource
|
||||
private QdrantVectorService qdrantVectorService;
|
||||
|
||||
|
||||
|
||||
@GetMapping("/test")
|
||||
@Operation(summary = "测试接口", description = "测试接口")
|
||||
public BaseResponse<String> testDemo(){
|
||||
return ResultUtils.success("hello world");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping("/talk")
|
||||
@Operation(summary = "测试聊天接口", description = "测试接口")
|
||||
@Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something")
|
||||
public Flux<String> testTalk(@DefaultValue("you are so cute!") String userInput){
|
||||
return client
|
||||
.prompt("""
|
||||
You're a 25-year-old guy—witty and laid-back, always replying in English.
|
||||
|
||||
Read the user's last message, then write three short and funny replies that sound like something a guy would say.
|
||||
Go easy on the emojis.
|
||||
Keep each under 20 words.
|
||||
|
||||
User message: %s
|
||||
""".formatted(userInput))
|
||||
.system("""
|
||||
Format rules (very important):
|
||||
- Return EXACTLY 3 replies.
|
||||
- Use "<SPLIT>" as the separator between replies.
|
||||
- Output format: reply1<SPLIT>reply2<SPLIT>reply3
|
||||
- Do NOT use "<SPLIT>" inside any reply.
|
||||
""")
|
||||
.user(userInput)
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.user(StpUtil.getLoginIdAsString())// ✅ 这里每次请求都会重新取当前登录用户
|
||||
.build())
|
||||
.stream()
|
||||
.content();
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/embed")
|
||||
@Operation(summary = "测试向量接口", description = "测试向量接口")
|
||||
@Parameter(name = "userInput",required = true,description = "测试向量接口",example = "you are so cute!")
|
||||
public BaseResponse<Embedding> testEmbed(@DefaultValue("you are so cute!") @RequestBody List<String> userInput){
|
||||
EmbeddingResponse response = embeddingModel.embedForResponse(userInput);
|
||||
return ResultUtils.success(response.getResult());
|
||||
}
|
||||
|
||||
|
||||
// @PostMapping("/testSaveEmbed")
|
||||
// @Operation(summary = "测试存储向量接口", description = "测试存储向量接口")
|
||||
// @Parameter(name = "userInput",required = true,description = "测试存储向量接口")
|
||||
// public BaseResponse<Boolean> testSaveEmbed(@RequestBody EmbedSaveReq embedSaveReq) {
|
||||
// qdrantVectorService.upsertPoint(embedSaveReq.getRecordItem().getId()
|
||||
// , embedSaveReq.getVector()
|
||||
// , JSONUtil.toJsonStr(embedSaveReq.getRecordItem()));
|
||||
// return ResultUtils.success(true);
|
||||
// }
|
||||
|
||||
|
||||
// @PostMapping("/testSearch")
|
||||
// @Operation(summary = "测试搜索向量接口", description = "测试搜索向量接口")
|
||||
// @Parameter(name = "userInput",required = true,description = "测试搜索向量接口")
|
||||
// public BaseResponse<List<QdrantSearchItem>> testSearch(@RequestBody SearchEmbedReq searchEmbedReq) {
|
||||
// return ResultUtils.success(qdrantVectorService.searchPoint(searchEmbedReq.getUserInputEmbed(), 3));
|
||||
// }
|
||||
|
||||
|
||||
@PostMapping("/tsetSearchText")
|
||||
@Operation(summary = "测试搜索语义接口", description = "测试搜索语义接口")
|
||||
@Parameter(name = "userInput",required = true,description = "测试搜索语义接口")
|
||||
public BaseResponse<List<QdrantSearchItem>> testSearchText(@RequestBody TextSearchReq textSearchReq) {
|
||||
return ResultUtils.success(qdrantVectorService.searchText(textSearchReq.getUserInput()));
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,7 @@ public class FileController {
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "上传文件", description = "上传文件接口")
|
||||
@Parameter(name = "file",required = true,description = "上传的文件")
|
||||
public BaseResponse<String> upload(@RequestParam("file") MultipartFile file){
|
||||
public BaseResponse<String> upload(@RequestPart("file") MultipartFile file){
|
||||
String fileUrl = fileService.upload(file);
|
||||
return ResultUtils.success(fileUrl);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.vo.products.KeyboardProductItemRespVO;
|
||||
import com.yolo.keyborad.service.KeyboardProductItemsService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/12
|
||||
*/
|
||||
@RestController
|
||||
@Slf4j
|
||||
@RequestMapping("/products")
|
||||
@Tag(name = "商品", description = "商品相关接口")
|
||||
public class ProductsController {
|
||||
|
||||
@Resource
|
||||
private KeyboardProductItemsService productItemsService;
|
||||
|
||||
@GetMapping("/detail")
|
||||
@Operation(summary = "查询商品明细", description = "根据商品ID或productId查询商品详情")
|
||||
public BaseResponse<KeyboardProductItemRespVO> getProductDetail(
|
||||
@RequestParam(value = "id", required = false) Long id,
|
||||
@RequestParam(value = "productId", required = false) String productId
|
||||
) {
|
||||
if (id == null && (productId == null || productId.isBlank())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "id 或 productId 至少传一个");
|
||||
}
|
||||
KeyboardProductItemRespVO result = (id != null)
|
||||
? productItemsService.getProductDetailById(id)
|
||||
: productItemsService.getProductDetailByProductId(productId);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/listByType")
|
||||
@Operation(summary = "按类型查询商品列表", description = "根据商品类型查询商品列表,type=all 返回全部")
|
||||
public BaseResponse<List<KeyboardProductItemRespVO>> listByType(@RequestParam("type") String type) {
|
||||
if (type == null || type.isBlank()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "type 不能为空");
|
||||
}
|
||||
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType(type);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/inApp/list")
|
||||
@Operation(summary = "查询内购商品列表", description = "查询 type=in-app-purchase 的商品列表")
|
||||
public BaseResponse<List<KeyboardProductItemRespVO>> listInAppPurchases() {
|
||||
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType("in-app-purchase");
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/subscription/list")
|
||||
@Operation(summary = "查询订阅商品列表", description = "查询 type=subscription 的商品列表")
|
||||
public BaseResponse<List<KeyboardProductItemRespVO>> listSubscriptions() {
|
||||
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType("subscription");
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.model.vo.SpeechToTextVO;
|
||||
import com.yolo.keyborad.service.DeepgramService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 语音服务控制器
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@RestController
|
||||
@Slf4j
|
||||
@RequestMapping("/speech")
|
||||
@Tag(name = "语音服务", description = "语音相关功能接口")
|
||||
public class SpeechController {
|
||||
|
||||
@Resource
|
||||
private DeepgramService deepgramService;
|
||||
|
||||
@PostMapping("/transcribe")
|
||||
@Operation(summary = "语音转文字", description = "上传音频文件并转换为文本")
|
||||
public BaseResponse<SpeechToTextVO> transcribe(@RequestPart("file") MultipartFile file) {
|
||||
SpeechToTextVO result = deepgramService.transcribe(file);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
}
|
||||
@@ -85,10 +85,26 @@ public class ThemesController {
|
||||
|
||||
@GetMapping("/recommended")
|
||||
@Operation(summary = "推荐主题列表", description = "按真实下载数量降序返回推荐主题")
|
||||
public BaseResponse<List<KeyboardThemesRespVO>> getRecommendedThemes() {
|
||||
public BaseResponse<List<KeyboardThemesRespVO>> getRecommendedThemes(@RequestParam(required = false) Long themeId) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
List<KeyboardThemesRespVO> result = themesService.getRecommendedThemes(userId);
|
||||
List<KeyboardThemesRespVO> result = themesService.getRecommendedThemes(userId, themeId);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
@Operation(summary = "搜索主题", description = "根据主题名称模糊搜索主题")
|
||||
public BaseResponse<List<KeyboardThemesRespVO>> searchThemes(@RequestParam String themeName) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
List<KeyboardThemesRespVO> result = themesService.searchThemesByName(themeName, userId);
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/restore")
|
||||
@Operation(summary = "恢复已删除的主题", description = "将用户已删除的主题重新展示")
|
||||
public BaseResponse<Void> restoreTheme(@RequestParam Long themeId) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
themePurchaseService.restoreDeletedTheme(userId, themeId);
|
||||
return ResultUtils.success(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,20 +6,22 @@ import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.model.dto.AppleLoginReq;
|
||||
import com.yolo.keyborad.model.dto.user.*;
|
||||
import com.yolo.keyborad.model.entity.KeyboardFeedback;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserInviteCodes;
|
||||
import com.yolo.keyborad.model.vo.user.InviteCodeRespVO;
|
||||
import com.yolo.keyborad.model.vo.user.KeyboardUserInfoRespVO;
|
||||
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
|
||||
import com.yolo.keyborad.service.IAppleService;
|
||||
import com.yolo.keyborad.service.KeyboardFeedbackService;
|
||||
import com.yolo.keyborad.service.KeyboardUserInviteCodesService;
|
||||
import com.yolo.keyborad.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -42,6 +44,11 @@ public class UserController {
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
private KeyboardFeedbackService feedbackService;
|
||||
|
||||
@Resource
|
||||
private KeyboardUserInviteCodesService inviteCodesService;
|
||||
/**
|
||||
* 苹果登录
|
||||
*
|
||||
@@ -57,6 +64,7 @@ public class UserController {
|
||||
@GetMapping("/logout")
|
||||
@Operation(summary = "退出登录", description = "退出登录接口")
|
||||
public BaseResponse<Boolean> logout() {
|
||||
StpUtil.logout(StpUtil.getLoginIdAsLong());
|
||||
StpUtil.logoutByTokenValue(StpUtil.getTokenValue());
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
@@ -94,8 +102,7 @@ public class UserController {
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "用户注册",description = "用户注册接口")
|
||||
public BaseResponse<Boolean> register(@RequestBody UserRegisterDTO userRegisterDTO) {
|
||||
userService.userRegister(userRegisterDTO);
|
||||
return ResultUtils.success(true);
|
||||
return ResultUtils.success(userService.userRegister(userRegisterDTO));
|
||||
}
|
||||
|
||||
@PostMapping("/sendVerifyMail")
|
||||
@@ -116,4 +123,26 @@ public class UserController {
|
||||
public BaseResponse<Boolean> resetPassWord(@RequestBody ResetPassWordDTO resetPassWordDTO) {
|
||||
return ResultUtils.success(userService.resetPassWord(resetPassWordDTO));
|
||||
}
|
||||
|
||||
@PostMapping("/feedback")
|
||||
@Operation(summary = "提交反馈", description = "用户提交反馈接口")
|
||||
public BaseResponse<Boolean> submitFeedback(@RequestBody FeedbackSubmitReq req) {
|
||||
KeyboardFeedback feedback = new KeyboardFeedback();
|
||||
feedback.setContent(req.getContent());
|
||||
feedback.setCreatedAt(new java.util.Date());
|
||||
return ResultUtils.success(feedbackService.save(feedback));
|
||||
}
|
||||
|
||||
@PostMapping("/bindInviteCode")
|
||||
@Operation(summary = "绑定邀请码", description = "用户填写邀请码进行绑定")
|
||||
public BaseResponse<Boolean> bindInviteCode(@RequestBody BindInviteCodeDTO bindInviteCodeDTO) {
|
||||
return ResultUtils.success(userService.bindInviteCode(bindInviteCodeDTO));
|
||||
}
|
||||
|
||||
@GetMapping("/inviteCode")
|
||||
@Operation(summary = "查询邀请码", description = "查询用户自己的邀请码")
|
||||
public BaseResponse<InviteCodeRespVO> getInviteCode() {
|
||||
long userId = StpUtil.getLoginIdAsLong();
|
||||
return ResultUtils.success( inviteCodesService.getUserInviteCode(userId));
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.model.dto.PageDTO;
|
||||
import com.yolo.keyborad.model.vo.wallet.KeyboardUserWalletRespVO;
|
||||
import com.yolo.keyborad.model.vo.wallet.WalletTransactionRespVO;
|
||||
import com.yolo.keyborad.service.KeyboardUserWalletService;
|
||||
import com.yolo.keyborad.service.KeyboardWalletTransactionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
@@ -26,6 +28,9 @@ public class WalletController {
|
||||
@Resource
|
||||
private KeyboardUserWalletService walletService;
|
||||
|
||||
@Resource
|
||||
private KeyboardWalletTransactionService transactionService;
|
||||
|
||||
@GetMapping("/balance")
|
||||
@Operation(summary = "查询钱包余额", description = "查询当前登录用户的钱包余额")
|
||||
public BaseResponse<KeyboardUserWalletRespVO> getBalance() {
|
||||
@@ -33,4 +38,12 @@ public class WalletController {
|
||||
KeyboardUserWalletRespVO balance = walletService.getWalletBalance(userId);
|
||||
return ResultUtils.success(balance);
|
||||
}
|
||||
|
||||
@PostMapping("/transactions")
|
||||
@Operation(summary = "分页查询钱包交易记录", description = "分页查询当前用户的钱包交易记录")
|
||||
public BaseResponse<IPage<WalletTransactionRespVO>> getTransactions(@RequestBody PageDTO pageDTO) {
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
IPage<WalletTransactionRespVO> transactions = transactionService.getUserTransactions(userId, pageDTO.getPageNum(), pageDTO.getPageSize());
|
||||
return ResultUtils.success(transactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.support.MissingServletRequestPartException;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
@@ -25,6 +26,20 @@ public class GlobalExceptionHandler {
|
||||
this.i18nService = i18nService;
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingServletRequestPartException.class)
|
||||
public BaseResponse<?> missingServletRequestPartExceptionHandler(MissingServletRequestPartException e, HttpServletRequest request) {
|
||||
log.error("missingServletRequestPartException: " + e.getMessage(), e);
|
||||
|
||||
String acceptLanguage = request.getHeader("Accept-Language");
|
||||
String errorMessage = i18nService.getMessageWithAcceptLanguage(String.valueOf(ErrorCode.FILE_IS_EMPTY.getCode()), acceptLanguage);
|
||||
|
||||
if (errorMessage == null) {
|
||||
errorMessage = ErrorCode.FILE_IS_EMPTY.getMessage();
|
||||
}
|
||||
|
||||
return ResultUtils.error(ErrorCode.FILE_IS_EMPTY.getCode(), errorMessage);
|
||||
}
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public BaseResponse<?> businessExceptionHandler(BusinessException e, HttpServletRequest request) {
|
||||
log.error("businessException: " + e.getMessage(), e);
|
||||
|
||||
@@ -18,15 +18,27 @@ public class RequestBodyCacheFilter extends OncePerRequestFilter {
|
||||
FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 只缓存一次
|
||||
// 获取请求的内容类型
|
||||
String contentType = request.getContentType();
|
||||
|
||||
// 跳过 multipart 请求,避免破坏文件上传功能
|
||||
if (contentType != null && contentType.toLowerCase().startsWith("multipart/")) {
|
||||
// 对于文件上传请求,直接放行,不进行请求体缓存
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经进行过请求体缓存,避免重复缓存
|
||||
if (!(request instanceof CachedBodyHttpServletRequest)) {
|
||||
|
||||
// 创建缓存请求对象,包装原始请求以支持多次读取请求体
|
||||
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request);
|
||||
|
||||
|
||||
// 使用缓存的请求对象继续执行过滤器链
|
||||
filterChain.doFilter(cachedRequest, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经是缓存过的请求,则直接执行过滤器链
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.yolo.keyborad.listener;
|
||||
|
||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||
import com.yolo.keyborad.service.KeyboardCharacterService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 人设缓存初始化器
|
||||
* 在应用启动时将所有人设缓存到Redis
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class CharacterCacheInitializer implements ApplicationRunner {
|
||||
|
||||
private static final String CHARACTER_CACHE_KEY = "character:";
|
||||
|
||||
@Resource
|
||||
private KeyboardCharacterService characterService;
|
||||
|
||||
@Resource(name = "objectRedisTemplate")
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
try {
|
||||
log.info("开始缓存人设列表到Redis...");
|
||||
List<KeyboardCharacter> characters = characterService.list();
|
||||
for (KeyboardCharacter character : characters) {
|
||||
String key = CHARACTER_CACHE_KEY + character.getId();
|
||||
redisTemplate.opsForValue().set(key, character, 7, TimeUnit.DAYS);
|
||||
}
|
||||
log.info("人设列表缓存完成,共缓存 {} 条记录", characters.size());
|
||||
} catch (Exception e) {
|
||||
log.error("缓存人设列表失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.yolo.keyborad.listener;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.yolo.keyborad.model.entity.KeyboardThemeStyles;
|
||||
import com.yolo.keyborad.model.entity.KeyboardThemes;
|
||||
import com.yolo.keyborad.model.vo.themes.KeyboardThemeStylesRespVO;
|
||||
import com.yolo.keyborad.model.vo.themes.KeyboardThemesRespVO;
|
||||
import com.yolo.keyborad.service.KeyboardThemeStylesService;
|
||||
import com.yolo.keyborad.service.KeyboardThemesService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 主题缓存初始化器
|
||||
* 在应用启动时按风格将主题缓存到Redis
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ThemeCacheInitializer implements ApplicationRunner {
|
||||
|
||||
/**
|
||||
* 主题按风格分组的缓存key前缀
|
||||
*/
|
||||
private static final String THEME_STYLE_KEY = "theme:style:";
|
||||
|
||||
/**
|
||||
* 所有风格列表的缓存key
|
||||
*/
|
||||
private static final String THEME_STYLES_KEY = "theme:styles";
|
||||
|
||||
/**
|
||||
* 所有主题列表的缓存key(风格ID=9999表示全部)
|
||||
*/
|
||||
private static final String THEME_ALL_KEY = "theme:style:all";
|
||||
|
||||
/**
|
||||
* 缓存过期时间(天)
|
||||
*/
|
||||
private static final long CACHE_EXPIRE_DAYS = 1;
|
||||
|
||||
@Resource
|
||||
private KeyboardThemesService themesService;
|
||||
|
||||
@Resource
|
||||
private KeyboardThemeStylesService themeStylesService;
|
||||
|
||||
@Resource(name = "objectRedisTemplate")
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
try {
|
||||
log.info("开始缓存主题数据到Redis...");
|
||||
|
||||
// 1. 缓存所有风格列表
|
||||
cacheAllStyles();
|
||||
|
||||
// 2. 按风格分组缓存主题
|
||||
cacheThemesByStyle();
|
||||
|
||||
log.info("主题数据缓存完成");
|
||||
} catch (Exception e) {
|
||||
log.error("缓存主题数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存所有风格列表
|
||||
*/
|
||||
private void cacheAllStyles() {
|
||||
List<KeyboardThemeStyles> stylesList = themeStylesService.lambdaQuery()
|
||||
.eq(KeyboardThemeStyles::getDeleted, false)
|
||||
.list();
|
||||
|
||||
List<KeyboardThemeStylesRespVO> stylesVOList = BeanUtil.copyToList(stylesList, KeyboardThemeStylesRespVO.class);
|
||||
redisTemplate.opsForValue().set(THEME_STYLES_KEY, stylesVOList, CACHE_EXPIRE_DAYS, TimeUnit.DAYS);
|
||||
log.info("已缓存 {} 种主题风格", stylesVOList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按风格分组缓存主题
|
||||
*/
|
||||
private void cacheThemesByStyle() {
|
||||
// 查询所有有效主题
|
||||
List<KeyboardThemes> allThemes = themesService.lambdaQuery()
|
||||
.eq(KeyboardThemes::getDeleted, false)
|
||||
.eq(KeyboardThemes::getThemeStatus, true)
|
||||
.orderByAsc(KeyboardThemes::getSort)
|
||||
.list();
|
||||
|
||||
// 转换为VO(不设置购买状态,缓存的是公共数据)
|
||||
List<KeyboardThemesRespVO> allThemesVO = allThemes.stream()
|
||||
.map(theme -> BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 缓存所有主题(风格ID=all)
|
||||
redisTemplate.opsForValue().set(THEME_ALL_KEY, allThemesVO, CACHE_EXPIRE_DAYS, TimeUnit.DAYS);
|
||||
log.info("已缓存所有主题,共 {} 个", allThemesVO.size());
|
||||
|
||||
// 按风格分组
|
||||
Map<Long, List<KeyboardThemesRespVO>> themesByStyle = allThemesVO.stream()
|
||||
.collect(Collectors.groupingBy(KeyboardThemesRespVO::getThemeStyle));
|
||||
|
||||
// 按风格缓存主题
|
||||
for (Map.Entry<Long, List<KeyboardThemesRespVO>> entry : themesByStyle.entrySet()) {
|
||||
Long styleId = entry.getKey();
|
||||
List<KeyboardThemesRespVO> themes = entry.getValue();
|
||||
String key = THEME_STYLE_KEY + styleId;
|
||||
redisTemplate.opsForValue().set(key, themes, CACHE_EXPIRE_DAYS, TimeUnit.DAYS);
|
||||
log.info("已缓存风格ID={} 的主题,共 {} 个", styleId, themes.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动刷新缓存(可通过接口调用)
|
||||
*/
|
||||
public void refreshCache() {
|
||||
log.info("手动刷新主题缓存...");
|
||||
clearCache();
|
||||
cacheAllStyles();
|
||||
cacheThemesByStyle();
|
||||
log.info("主题缓存刷新完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除主题相关缓存
|
||||
*/
|
||||
public void clearCache() {
|
||||
// 删除风格列表缓存
|
||||
redisTemplate.delete(THEME_STYLES_KEY);
|
||||
redisTemplate.delete(THEME_ALL_KEY);
|
||||
|
||||
// 删除所有风格下的主题缓存
|
||||
var keys = redisTemplate.keys(THEME_STYLE_KEY + "*");
|
||||
if (keys != null && !keys.isEmpty()) {
|
||||
redisTemplate.delete(keys);
|
||||
}
|
||||
log.info("已清除主题相关缓存");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiChatMessage;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26 17:00
|
||||
*/
|
||||
|
||||
public interface KeyboardAiChatMessageMapper extends BaseMapper<KeyboardAiChatMessage> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiChatSession;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/28 16:20
|
||||
*/
|
||||
|
||||
public interface KeyboardAiChatSessionMapper extends BaseMapper<KeyboardAiChatSession> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanionCommentLike;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26 20:57
|
||||
*/
|
||||
|
||||
public interface KeyboardAiCompanionCommentLikeMapper extends BaseMapper<KeyboardAiCompanionCommentLike> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanionComment;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26 20:31
|
||||
*/
|
||||
|
||||
public interface KeyboardAiCompanionCommentMapper extends BaseMapper<KeyboardAiCompanionComment> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanionLike;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/27 18:18
|
||||
*/
|
||||
|
||||
public interface KeyboardAiCompanionLikeMapper extends BaseMapper<KeyboardAiCompanionLike> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26 13:51
|
||||
*/
|
||||
|
||||
public interface KeyboardAiCompanionMapper extends BaseMapper<KeyboardAiCompanion> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanionReport;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/29 16:17
|
||||
*/
|
||||
|
||||
public interface KeyboardAiCompanionReportMapper extends BaseMapper<KeyboardAiCompanionReport> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardFeedback;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/17 17:06
|
||||
*/
|
||||
|
||||
public interface KeyboardFeedbackMapper extends BaseMapper<KeyboardFeedback> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardProductItems;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/12 13:44
|
||||
*/
|
||||
|
||||
public interface KeyboardProductItemsMapper extends BaseMapper<KeyboardProductItems> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserCallLog;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/17 13:29
|
||||
*/
|
||||
|
||||
public interface KeyboardUserCallLogMapper extends BaseMapper<KeyboardUserCallLog> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserInviteCodes;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/18 16:26
|
||||
*/
|
||||
|
||||
public interface KeyboardUserInviteCodesMapper extends BaseMapper<KeyboardUserInviteCodes> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserInvites;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/19 13:26
|
||||
*/
|
||||
|
||||
public interface KeyboardUserInvitesMapper extends BaseMapper<KeyboardUserInvites> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserPurchaseRecords;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/12 15:16
|
||||
*/
|
||||
|
||||
public interface KeyboardUserPurchaseRecordsMapper extends BaseMapper<KeyboardUserPurchaseRecords> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yolo.keyborad.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/16 16:00
|
||||
*/
|
||||
|
||||
public interface KeyboardUserQuotaTotalMapper extends BaseMapper<KeyboardUserQuotaTotal> {
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yolo.keyborad.model.entity.KeyboardWalletTransaction;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/10 18:54
|
||||
*/
|
||||
|
||||
* @author: ziin
|
||||
* @date: 2025/12/22 18:10
|
||||
*/
|
||||
|
||||
public interface KeyboardWalletTransactionMapper extends BaseMapper<KeyboardWalletTransaction> {
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yolo.keyborad.model.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Apple 服务器通知(精简字段)
|
||||
*/
|
||||
@Data
|
||||
public class AppleServerNotification {
|
||||
|
||||
@JsonProperty("notification_type")
|
||||
private String notificationType;
|
||||
|
||||
@JsonProperty("auto_renew_status")
|
||||
private String autoRenewStatus;
|
||||
|
||||
@JsonProperty("app_account_token")
|
||||
private String appAccountToken;
|
||||
|
||||
@JsonProperty("original_transaction_id")
|
||||
private String originalTransactionId;
|
||||
|
||||
@JsonProperty("product_id")
|
||||
private String productId;
|
||||
|
||||
@JsonProperty("purchase_date")
|
||||
private String purchaseDate;
|
||||
|
||||
@JsonProperty("expires_date")
|
||||
private String expiresDate;
|
||||
|
||||
@JsonProperty("environment")
|
||||
private String environment;
|
||||
|
||||
@JsonProperty("transaction_id")
|
||||
private String transactionId;
|
||||
|
||||
@JsonProperty("signed_transaction_info")
|
||||
private String signedTransactionInfo;
|
||||
}
|
||||
18
src/main/java/com/yolo/keyborad/model/dto/PageDTO.java
Normal file
18
src/main/java/com/yolo/keyborad/model/dto/PageDTO.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.yolo.keyborad.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/22 18:36
|
||||
*/
|
||||
@Data
|
||||
public class PageDTO {
|
||||
|
||||
@Schema(description = "页码")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
@Schema(description = "每页数量")
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yolo.keyborad.model.dto.chat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "聊天记录分页查询请求")
|
||||
public class ChatHistoryPageReq {
|
||||
|
||||
@Schema(description = "AI陪聊角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long companionId;
|
||||
|
||||
@Schema(description = "页码", example = "1")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
@Schema(description = "每页数量", example = "20")
|
||||
private Integer pageSize = 20;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yolo.keyborad.model.dto.chat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/8 15:05
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "同步对话请求")
|
||||
public class ChatMessageReq {
|
||||
|
||||
@Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String content;
|
||||
|
||||
@Schema(description = "AI陪聊角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long companionId;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.yolo.keyborad.model.dto.chat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/28
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "会话重置请求")
|
||||
public class SessionResetReq {
|
||||
|
||||
@Schema(description = "AI陪聊角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long companionId;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yolo.keyborad.model.dto.comment;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "发表评论请求")
|
||||
public class CommentAddReq {
|
||||
|
||||
@Schema(description = "被评论的AI陪伴角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long companionId;
|
||||
|
||||
@Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String content;
|
||||
|
||||
@Schema(description = "父评论ID,NULL表示一级评论")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "根评论ID,用于标识同一评论线程")
|
||||
private Long rootId;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.yolo.keyborad.model.dto.comment;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "评论点赞请求")
|
||||
public class CommentLikeReq {
|
||||
|
||||
@Schema(description = "评论ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long commentId;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yolo.keyborad.model.dto.comment;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "评论分页查询请求")
|
||||
public class CommentPageReq {
|
||||
|
||||
@Schema(description = "AI陪伴角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long companionId;
|
||||
|
||||
@Schema(description = "页码", example = "1")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
@Schema(description = "每页数量", example = "20")
|
||||
private Integer pageSize = 20;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.yolo.keyborad.model.dto.companion;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/27
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "AI角色点赞请求")
|
||||
public class CompanionLikeReq {
|
||||
|
||||
@Schema(description = "AI角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long companionId;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yolo.keyborad.model.dto.companion;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/29
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "AI角色举报请求")
|
||||
public class CompanionReportReq {
|
||||
|
||||
@Schema(description = "AI角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long companionId;
|
||||
|
||||
@Schema(description = "举报类型列表:1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他,支持多选", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<Short> reportTypes;
|
||||
|
||||
@Schema(description = "详细描述")
|
||||
private String reportDesc;
|
||||
|
||||
@Schema(description = "聊天上下文快照JSON")
|
||||
private String chatContext;
|
||||
|
||||
@Schema(description = "图片证据URL")
|
||||
private String evidenceImageUrl;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yolo.keyborad.model.dto.user;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 绑定邀请码请求
|
||||
* @author: ziin
|
||||
* @date: 2025/12/19
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "绑定邀请码请求")
|
||||
public class BindInviteCodeDTO {
|
||||
|
||||
/**
|
||||
* 邀请码
|
||||
*/
|
||||
@Schema(description = "邀请码", required = true)
|
||||
private String inviteCode;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.yolo.keyborad.model.dto.user;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 用户反馈提交请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "用户反馈提交请求")
|
||||
public class FeedbackSubmitReq {
|
||||
|
||||
/**
|
||||
* 反馈内容
|
||||
*/
|
||||
@Schema(description = "反馈内容", required = true)
|
||||
private String content;
|
||||
}
|
||||
@@ -24,4 +24,7 @@ public class UserRegisterDTO {
|
||||
|
||||
@Schema(description = "验证码")
|
||||
private String verifyCode;
|
||||
|
||||
@Schema(description = "邀请码(可选)")
|
||||
private String inviteCode;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import lombok.Data;
|
||||
|
||||
@Schema(description="多语言消息表")
|
||||
@Data
|
||||
@TableName("i18n_message")
|
||||
@TableName("keyboard_i18n_message")
|
||||
public class I18nMessage {
|
||||
@TableId("id")
|
||||
@Schema(description="主键")
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26 17:00
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户与AI情感陪伴角色的聊天记录表
|
||||
*/
|
||||
@Schema(description="用户与AI情感陪伴角色的聊天记录表")
|
||||
@Data
|
||||
@TableName(value = "keyboard_ai_chat_message")
|
||||
public class KeyboardAiChatMessage {
|
||||
/**
|
||||
* 聊天消息唯一ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="聊天消息唯一ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description="用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 陪伴角色ID
|
||||
*/
|
||||
@TableField(value = "companion_id")
|
||||
@Schema(description="陪伴角色ID")
|
||||
private Long companionId;
|
||||
|
||||
/**
|
||||
* 消息发送方:1=用户,2=AI
|
||||
*/
|
||||
@TableField(value = "sender")
|
||||
@Schema(description="消息发送方:1=用户,2=AI")
|
||||
private Short sender;
|
||||
|
||||
/**
|
||||
* 聊天消息内容
|
||||
*/
|
||||
@TableField(value = "content")
|
||||
@Schema(description="聊天消息内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* AI识别到的用户情绪
|
||||
*/
|
||||
@TableField(value = "emotion_detected")
|
||||
@Schema(description="AI识别到的用户情绪")
|
||||
private String emotionDetected;
|
||||
|
||||
/**
|
||||
* AI提供的支持类型(倾听/共情/安抚等)
|
||||
*/
|
||||
@TableField(value = "support_type")
|
||||
@Schema(description="AI提供的支持类型(倾听/共情/安抚等)")
|
||||
private String supportType;
|
||||
|
||||
/**
|
||||
* 消息创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="消息创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@TableField(value = "session_id")
|
||||
@Schema(description = "会话Id")
|
||||
private Long sessionId;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/28 16:20
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户与AI陪伴角色的聊天会话表,用于支持聊天重置与关系重启
|
||||
*/
|
||||
@Schema(description="用户与AI陪伴角色的聊天会话表,用于支持聊天重置与关系重启")
|
||||
@Data
|
||||
@TableName(value = "keyboard_ai_chat_session")
|
||||
public class KeyboardAiChatSession {
|
||||
/**
|
||||
* 聊天会话唯一ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="聊天会话唯一ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description="用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 陪伴角色ID
|
||||
*/
|
||||
@TableField(value = "companion_id")
|
||||
@Schema(description="陪伴角色ID")
|
||||
private Long companionId;
|
||||
|
||||
/**
|
||||
* 会话重置版本号,用于标识第几次重新开始陪伴关系
|
||||
*/
|
||||
@TableField(value = "reset_version")
|
||||
@Schema(description="会话重置版本号,用于标识第几次重新开始陪伴关系")
|
||||
private Integer resetVersion;
|
||||
|
||||
/**
|
||||
* 是否为当前活跃会话(true=当前使用中)
|
||||
*/
|
||||
@TableField(value = "is_active")
|
||||
@Schema(description="是否为当前活跃会话(true=当前使用中)")
|
||||
private Boolean isActive;
|
||||
|
||||
/**
|
||||
* 会话创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="会话创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 会话结束时间(用户重置或系统关闭会话时记录)
|
||||
*/
|
||||
@TableField(value = "ended_at")
|
||||
@Schema(description="会话结束时间(用户重置或系统关闭会话时记录)")
|
||||
private Date endedAt;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26 13:51
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI陪聊角色表,用于定义恋爱/陪伴型虚拟角色的基础信息与人设
|
||||
*/
|
||||
@Schema(description="AI陪聊角色表,用于定义恋爱/陪伴型虚拟角色的基础信息与人设")
|
||||
@Data
|
||||
@TableName(value = "keyboard_ai_companion")
|
||||
public class KeyboardAiCompanion {
|
||||
/**
|
||||
* 陪聊角色唯一ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="陪聊角色唯一ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 角色名称(展示用,如:Katie Leona)
|
||||
*/
|
||||
@TableField(value = "\"name\"")
|
||||
@Schema(description="角色名称(展示用,如:Katie Leona)")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 角色头像URL,用于列表页和聊天页
|
||||
*/
|
||||
@TableField(value = "avatar_url")
|
||||
@Schema(description="角色头像URL,用于列表页和聊天页")
|
||||
private String avatarUrl;
|
||||
|
||||
/**
|
||||
* 角色封面图URL,用于角色详情页
|
||||
*/
|
||||
@TableField(value = "cover_image_url")
|
||||
@Schema(description="角色封面图URL,用于角色详情页")
|
||||
private String coverImageUrl;
|
||||
|
||||
/**
|
||||
* 角色性别(male / female / other)
|
||||
*/
|
||||
@TableField(value = "gender")
|
||||
@Schema(description="角色性别(male / female / other)")
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 角色年龄段描述(如:20s、25-30)
|
||||
*/
|
||||
@TableField(value = "age_range")
|
||||
@Schema(description="角色年龄段描述(如:20s、25-30)")
|
||||
private String ageRange;
|
||||
|
||||
/**
|
||||
* 一句话人设描述,用于卡片或列表展示
|
||||
*/
|
||||
@TableField(value = "short_desc")
|
||||
@Schema(description="一句话人设描述,用于卡片或列表展示")
|
||||
private String shortDesc;
|
||||
|
||||
/**
|
||||
* 角色详细介绍文案,用于角色详情页
|
||||
*/
|
||||
@TableField(value = "intro_text")
|
||||
@Schema(description="角色详细介绍文案,用于角色详情页")
|
||||
private String introText;
|
||||
|
||||
/**
|
||||
* 角色性格标签数组(如:温柔、黏人、治愈)
|
||||
*/
|
||||
@TableField(value = "personality_tags")
|
||||
@Schema(description="角色性格标签数组(如:温柔、黏人、治愈)")
|
||||
private String personalityTags;
|
||||
|
||||
/**
|
||||
* 角色说话风格(如:撒娇型、理性型、活泼型)
|
||||
*/
|
||||
@TableField(value = "speaking_style")
|
||||
@Schema(description="角色说话风格(如:撒娇型、理性型、活泼型)")
|
||||
private String speakingStyle;
|
||||
|
||||
/**
|
||||
* AI系统Prompt,定义角色核心人设,仅供模型使用
|
||||
*/
|
||||
@TableField(value = "system_prompt")
|
||||
@Schema(description="AI系统Prompt,定义角色核心人设,仅供模型使用")
|
||||
private String systemPrompt;
|
||||
|
||||
/**
|
||||
* 角色状态:1=上线,0=下线
|
||||
*/
|
||||
@TableField(value = "\"status\"")
|
||||
@Schema(description="角色状态:1=上线,0=下线")
|
||||
private Short status;
|
||||
|
||||
/**
|
||||
* 角色可见性:1=公开,2=内测,3=隐藏
|
||||
*/
|
||||
@TableField(value = "visibility")
|
||||
@Schema(description="角色可见性:1=公开,2=内测,3=隐藏")
|
||||
private Short visibility;
|
||||
|
||||
/**
|
||||
* 排序权重,数值越大排序越靠前
|
||||
*/
|
||||
@TableField(value = "sort_order")
|
||||
@Schema(description="排序权重,数值越大排序越靠前")
|
||||
private Integer sortOrder;
|
||||
|
||||
/**
|
||||
* 角色热度评分,用于推荐排序
|
||||
*/
|
||||
@TableField(value = "popularity_score")
|
||||
@Schema(description="角色热度评分,用于推荐排序")
|
||||
private Integer popularityScore;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description="更新时间")
|
||||
private Date updatedAt;
|
||||
|
||||
@TableField(value = "prologue")
|
||||
@Schema(description="开场白")
|
||||
private String prologue;
|
||||
|
||||
@TableField(value = "prologue_audio")
|
||||
@Schema(description="开场白音频")
|
||||
private String prologueAudio;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26 20:31
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户对AI陪伴角色的评论表,支持多级评论结构(一级评论与回复)
|
||||
*/
|
||||
@Schema(description="用户对AI陪伴角色的评论表,支持多级评论结构(一级评论与回复)")
|
||||
@Data
|
||||
@TableName(value = "keyboard_ai_companion_comment")
|
||||
public class KeyboardAiCompanionComment {
|
||||
/**
|
||||
* 评论唯一ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="评论唯一ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 被评论的AI陪伴角色ID
|
||||
*/
|
||||
@TableField(value = "companion_id")
|
||||
@Schema(description="被评论的AI陪伴角色ID")
|
||||
private Long companionId;
|
||||
|
||||
/**
|
||||
* 发表评论的用户ID
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description="发表评论的用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 父评论ID,NULL表示一级评论
|
||||
*/
|
||||
@TableField(value = "parent_id")
|
||||
@Schema(description="父评论ID,NULL表示一级评论")
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 根评论ID,用于标识同一评论线程
|
||||
*/
|
||||
@TableField(value = "root_id")
|
||||
@Schema(description="根评论ID,用于标识同一评论线程")
|
||||
private Long rootId;
|
||||
|
||||
/**
|
||||
* 评论内容
|
||||
*/
|
||||
@TableField(value = "content")
|
||||
@Schema(description="评论内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 点赞数量
|
||||
*/
|
||||
@TableField(value = "\"like\"")
|
||||
@Schema(description="点赞数量")
|
||||
private Long like;
|
||||
|
||||
/**
|
||||
* 评论状态:1=正常,0=隐藏,-1=删除
|
||||
*/
|
||||
@TableField(value = "\"status\"")
|
||||
@Schema(description="评论状态:1=正常,0=隐藏,-1=删除")
|
||||
private Short status;
|
||||
|
||||
/**
|
||||
* 评论点赞数
|
||||
*/
|
||||
@TableField(value = "like_count")
|
||||
@Schema(description="评论点赞数")
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 评论创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="评论创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 评论更新时间
|
||||
*/
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description="评论更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26 20:57
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户对AI陪伴角色评论的点赞记录表,用于记录点赞与取消点赞行为
|
||||
*/
|
||||
@Schema(description = "用户对AI陪伴角色评论的点赞记录表,用于记录点赞与取消点赞行为")
|
||||
@Data
|
||||
@TableName(value = "keyboard_ai_companion_comment_like")
|
||||
public class KeyboardAiCompanionCommentLike {
|
||||
/**
|
||||
* 评论点赞记录唯一ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "评论点赞记录唯一ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 被点赞的评论ID
|
||||
*/
|
||||
@TableField(value = "comment_id")
|
||||
@Schema(description = "被点赞的评论ID")
|
||||
private Long commentId;
|
||||
|
||||
/**
|
||||
* 点赞用户ID
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description = "点赞用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 点赞状态:1=已点赞,0=已取消
|
||||
*/
|
||||
@TableField(value = "\"status\"")
|
||||
@Schema(description = "点赞状态:1=已点赞,0=已取消")
|
||||
private Short status;
|
||||
|
||||
/**
|
||||
* 点赞记录创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "点赞记录创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 点赞状态更新时间
|
||||
*/
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description = "点赞状态更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/27 18:18
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户对AI陪伴角色的点赞行为记录表,用于记录点赞与取消点赞
|
||||
*/
|
||||
@Schema(description="用户对AI陪伴角色的点赞行为记录表,用于记录点赞与取消点赞")
|
||||
@Data
|
||||
@TableName(value = "keyboard_ai_companion_like")
|
||||
public class KeyboardAiCompanionLike {
|
||||
/**
|
||||
* AI角色点赞记录唯一ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="AI角色点赞记录唯一ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 被点赞的AI陪伴角色ID
|
||||
*/
|
||||
@TableField(value = "companion_id")
|
||||
@Schema(description="被点赞的AI陪伴角色ID")
|
||||
private Long companionId;
|
||||
|
||||
/**
|
||||
* 执行点赞操作的用户ID
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description="执行点赞操作的用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 点赞状态:1=已点赞,0=已取消
|
||||
*/
|
||||
@TableField(value = "\"status\"")
|
||||
@Schema(description="点赞状态:1=已点赞,0=已取消")
|
||||
private Short status;
|
||||
|
||||
/**
|
||||
* 首次点赞时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="首次点赞时间")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 点赞状态最近更新时间
|
||||
*/
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description="点赞状态最近更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/29 16:17
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI角色举报记录表
|
||||
*/
|
||||
@Schema(description="AI角色举报记录表")
|
||||
@Data
|
||||
@TableName(value = "keyboard_ai_companion_report")
|
||||
public class KeyboardAiCompanionReport {
|
||||
/**
|
||||
* 举报记录唯一ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="举报记录唯一ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 被举报的AI角色ID(逻辑关联 keyboard_ai_companion.id,无物理外键)
|
||||
*/
|
||||
@TableField(value = "companion_id")
|
||||
@Schema(description="被举报的AI角色ID(逻辑关联 keyboard_ai_companion.id,无物理外键)")
|
||||
private Long companionId;
|
||||
|
||||
/**
|
||||
* 发起举报的用户ID(逻辑关联用户表)
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description="发起举报的用户ID(逻辑关联用户表)")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 举报类型:1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他,多选时逗号分隔
|
||||
*/
|
||||
@TableField(value = "report_type")
|
||||
@Schema(description="举报类型:1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他,多选时逗号分隔")
|
||||
private String reportType;
|
||||
|
||||
/**
|
||||
* 用户填写的详细举报描述
|
||||
*/
|
||||
@TableField(value = "report_desc")
|
||||
@Schema(description="用户填写的详细举报描述")
|
||||
private String reportDesc;
|
||||
|
||||
/**
|
||||
* 违规现场:举报时的聊天上下文快照(建议存JSON字符串),用于审核取证
|
||||
*/
|
||||
@TableField(value = "chat_context")
|
||||
@Schema(description="违规现场:举报时的聊天上下文快照(建议存JSON字符串),用于审核取证")
|
||||
private String chatContext;
|
||||
|
||||
/**
|
||||
* 图片证据:用户上传的截图URL
|
||||
*/
|
||||
@TableField(value = "evidence_image_url")
|
||||
@Schema(description="图片证据:用户上传的截图URL")
|
||||
private String evidenceImageUrl;
|
||||
|
||||
/**
|
||||
* 处理状态:0=待处理, 1=违规确立(已处罚), 2=无效举报/已驳回, 3=已忽略
|
||||
*/
|
||||
@TableField(value = "\"status\"")
|
||||
@Schema(description="处理状态:0=待处理, 1=违规确立(已处罚), 2=无效举报/已驳回, 3=已忽略")
|
||||
private Short status;
|
||||
|
||||
/**
|
||||
* 管理员处理备注(记录处理理由或处罚措施)
|
||||
*/
|
||||
@TableField(value = "admin_remark")
|
||||
@Schema(description="管理员处理备注(记录处理理由或处罚措施)")
|
||||
private String adminRemark;
|
||||
|
||||
/**
|
||||
* 举报提交时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="举报提交时间")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 最后更新时间
|
||||
*/
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description="最后更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/17 17:06
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户反馈表
|
||||
*/
|
||||
@Schema(description="用户反馈表")
|
||||
@Data
|
||||
@TableName(value = "keyboard_feedback")
|
||||
public class KeyboardFeedback {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户反馈内容
|
||||
*/
|
||||
@TableField(value = "content")
|
||||
@Schema(description="用户反馈内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 反馈创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="反馈创建时间")
|
||||
private Date createdAt;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/12 13:44
|
||||
*/
|
||||
|
||||
@Schema
|
||||
@Data
|
||||
@TableName(value = "keyboard_product_items")
|
||||
public class KeyboardProductItems {
|
||||
/**
|
||||
* 主键,自增,唯一标识每个产品项
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="主键,自增,唯一标识每个产品项")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 产品标识符,唯一标识每个产品(如 com.loveKey.nyx.2month)
|
||||
*/
|
||||
@TableField(value = "product_id")
|
||||
@Schema(description="产品标识符,唯一标识每个产品(如 com.loveKey.nyx.2month)")
|
||||
private String productId;
|
||||
|
||||
/**
|
||||
* 产品类型,区分订阅(subscription)和内购(in-app-purchase)
|
||||
*/
|
||||
@TableField(value = "\"type\"")
|
||||
@Schema(description="产品类型,区分订阅(subscription)和内购(in-app-purchase)")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 产品名称(如 100, 2)
|
||||
*/
|
||||
@TableField(value = "\"name\"")
|
||||
@Schema(description="产品名称(如 100, 2)")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 产品单位(如 金币,个月)
|
||||
*/
|
||||
@TableField(value = "unit")
|
||||
@Schema(description="产品单位(如 金币,个月)")
|
||||
private String unit;
|
||||
|
||||
/**
|
||||
* 订阅时长的数值部分(如 2)
|
||||
*/
|
||||
@TableField(value = "duration_value")
|
||||
@Schema(description="订阅时长的数值部分(如 2)")
|
||||
private Integer durationValue;
|
||||
|
||||
/**
|
||||
* 订阅时长的单位部分(如 月,天)
|
||||
*/
|
||||
@TableField(value = "duration_unit")
|
||||
@Schema(description="订阅时长的单位部分(如 月,天)")
|
||||
private String durationUnit;
|
||||
|
||||
/**
|
||||
* 产品价格
|
||||
*/
|
||||
@TableField(value = "price")
|
||||
@Schema(description="产品价格")
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 产品的货币单位,如美元($)
|
||||
*/
|
||||
@TableField(value = "currency")
|
||||
@Schema(description="产品的货币单位,如美元($)")
|
||||
private String currency;
|
||||
|
||||
/**
|
||||
* 产品的描述,提供更多细节信息
|
||||
*/
|
||||
@TableField(value = "description")
|
||||
@Schema(description="产品的描述,提供更多细节信息")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 产品项的创建时间,默认当前时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="产品项的创建时间,默认当前时间")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 产品项的最后更新时间,更新时自动设置为当前时间
|
||||
*/
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description="产品项的最后更新时间,更新时自动设置为当前时间")
|
||||
private Date updatedAt;
|
||||
|
||||
/**
|
||||
* 订阅时长的具体天数
|
||||
*/
|
||||
@TableField(value = "duration_days")
|
||||
@Schema(description="订阅时长的具体天数")
|
||||
private Integer durationDays;
|
||||
}
|
||||
@@ -121,4 +121,8 @@ public class KeyboardUser {
|
||||
@TableField(value = "vip_expiry")
|
||||
@Schema(description = "VIP 过期时间")
|
||||
private Date vipExpiry;
|
||||
|
||||
@TableField(value = "vip_level")
|
||||
@Schema(description = "vip等级")
|
||||
private Integer vipLevel;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/17 13:29
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户每次调用日志(用于记录token、模型、耗时、成功率等)
|
||||
*/
|
||||
@Schema(description="用户每次调用日志(用于记录token、模型、耗时、成功率等)")
|
||||
@Data
|
||||
@TableName(value = "keyboard_user_call_log")
|
||||
public class KeyboardUserCallLog {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description="用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 幂等请求ID,避免重试导致重复记录
|
||||
*/
|
||||
@TableField(value = "request_id")
|
||||
@Schema(description="幂等请求ID,避免重试导致重复记录")
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 调用功能来源
|
||||
*/
|
||||
@TableField(value = "feature")
|
||||
@Schema(description="调用功能来源")
|
||||
private String feature;
|
||||
|
||||
/**
|
||||
* 调用的模型名称
|
||||
*/
|
||||
@TableField(value = "model")
|
||||
@Schema(description="调用的模型名称")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 输入token数
|
||||
*/
|
||||
@TableField(value = "input_tokens")
|
||||
@Schema(description="输入token数")
|
||||
private Integer inputTokens;
|
||||
|
||||
/**
|
||||
* 输出token数
|
||||
*/
|
||||
@TableField(value = "output_tokens")
|
||||
@Schema(description="输出token数")
|
||||
private Integer outputTokens;
|
||||
|
||||
/**
|
||||
* 总token数(input+output)
|
||||
*/
|
||||
@TableField(value = "total_tokens")
|
||||
@Schema(description="总token数(input+output)")
|
||||
private Integer totalTokens;
|
||||
|
||||
/**
|
||||
* 调用是否成功
|
||||
*/
|
||||
@TableField(value = "success")
|
||||
@Schema(description="调用是否成功")
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 调用耗时(毫秒)
|
||||
*/
|
||||
@TableField(value = "latency_ms")
|
||||
@Schema(description="调用耗时(毫秒)")
|
||||
private Integer latencyMs;
|
||||
|
||||
/**
|
||||
* 失败错误码(可空)
|
||||
*/
|
||||
@TableField(value = "error_code")
|
||||
@Schema(description="失败错误码(可空)")
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 调用记录创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="调用记录创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@TableField(value = "gen_id")
|
||||
@Schema(description="生成 Id")
|
||||
private String genId;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/18 16:26
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户生成的邀请码表,用于邀请新用户注册/安装并建立邀请关系
|
||||
*/
|
||||
@Schema(description="用户生成的邀请码表,用于邀请新用户注册/安装并建立邀请关系")
|
||||
@Data
|
||||
@KeySequence("invite_codes_id_seq")
|
||||
@TableName(value = "keyboard_user_invite_codes")
|
||||
public class KeyboardUserInviteCodes {
|
||||
/**
|
||||
* 邀请码主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="邀请码主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 邀请码字符串,对外展示,唯一
|
||||
*/
|
||||
@TableField(value = "code")
|
||||
@Schema(description="邀请码字符串,对外展示,唯一")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 邀请码所属用户ID(邀请人)
|
||||
*/
|
||||
@TableField(value = "owner_user_id")
|
||||
@Schema(description="邀请码所属用户ID(邀请人)")
|
||||
private Long ownerUserId;
|
||||
|
||||
/**
|
||||
* 邀请码状态:1=启用,0=停用
|
||||
*/
|
||||
@TableField(value = "\"status\"")
|
||||
@Schema(description="邀请码状态:1=启用,0=停用")
|
||||
private Short status;
|
||||
|
||||
/**
|
||||
* 邀请码创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="邀请码创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 邀请码过期时间,NULL表示永久有效
|
||||
*/
|
||||
@TableField(value = "expires_at")
|
||||
@Schema(description="邀请码过期时间,NULL表示永久有效")
|
||||
private Date expiresAt;
|
||||
|
||||
/**
|
||||
* 邀请码最大可使用次数,NULL表示不限次数
|
||||
*/
|
||||
@TableField(value = "max_uses")
|
||||
@Schema(description="邀请码最大可使用次数,NULL表示不限次数")
|
||||
private Integer maxUses;
|
||||
|
||||
/**
|
||||
* 邀请码已使用次数
|
||||
*/
|
||||
@TableField(value = "used_count")
|
||||
@Schema(description="邀请码已使用次数")
|
||||
private Integer usedCount;
|
||||
|
||||
/**
|
||||
* 邀请码类型:USER=普通用户邀请码,TENANT=租户邀请码
|
||||
*/
|
||||
@TableField(value = "invite_type")
|
||||
@Schema(description="邀请码类型:USER=普通用户邀请码,AGENT=租户邀请码")
|
||||
private String inviteType;
|
||||
|
||||
/**
|
||||
* 邀请码所属租户ID(当inviteType=AGENT时使用)
|
||||
*/
|
||||
@TableField(value = "owner_tenant_id")
|
||||
@Schema(description="邀请码所属租户ID(当inviteType=AGENT时使用)")
|
||||
private Long ownerTenantId;
|
||||
|
||||
/**
|
||||
* 邀请码所属租户用户ID(当inviteType=AGENT时使用)
|
||||
*/
|
||||
@TableField(value = "owner_system_user_id")
|
||||
@Schema(description="邀请码所属租户用户ID(当inviteType=AGENT时使用)")
|
||||
private Long ownerSystemUserId;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/29 13:58
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户邀请关系绑定台账表,记录新用户最终归属的邀请人
|
||||
*/
|
||||
@Schema(description = "用户邀请关系绑定台账表,记录新用户最终归属的邀请人")
|
||||
@Data
|
||||
@TableName(value = "keyboard_user_invites")
|
||||
public class KeyboardUserInvites {
|
||||
/**
|
||||
* 邀请绑定记录主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "邀请绑定记录主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 邀请人用户ID
|
||||
*/
|
||||
@TableField(value = "inviter_user_id")
|
||||
@Schema(description = "邀请人用户ID")
|
||||
private Long inviterUserId;
|
||||
|
||||
/**
|
||||
* 被邀请人用户ID(新注册用户)
|
||||
*/
|
||||
@TableField(value = "invitee_user_id")
|
||||
@Schema(description = "被邀请人用户ID(新注册用户)")
|
||||
private Long inviteeUserId;
|
||||
|
||||
/**
|
||||
* 使用的邀请码ID
|
||||
*/
|
||||
@TableField(value = "invite_code_id")
|
||||
@Schema(description = "使用的邀请码ID")
|
||||
private Long inviteCodeId;
|
||||
|
||||
/**
|
||||
* 绑定时关联的点击Token(通过邀请链接自动绑定时使用)
|
||||
*/
|
||||
@TableField(value = "click_token")
|
||||
@Schema(description = "绑定时关联的点击Token(通过邀请链接自动绑定时使用)")
|
||||
private String clickToken;
|
||||
|
||||
/**
|
||||
* 绑定方式:1=手动填写邀请码,2=邀请链接自动绑定,3=其他方式
|
||||
*/
|
||||
@TableField(value = "bind_type")
|
||||
@Schema(description = "绑定方式:1=手动填写邀请码,2=邀请链接自动绑定,3=其他方式")
|
||||
private Short bindType;
|
||||
|
||||
/**
|
||||
* 邀请关系绑定完成时间
|
||||
*/
|
||||
@TableField(value = "bound_at")
|
||||
@Schema(description = "邀请关系绑定完成时间")
|
||||
private Date boundAt;
|
||||
|
||||
/**
|
||||
* 绑定 iP
|
||||
*/
|
||||
@TableField(value = "bind_ip")
|
||||
@Schema(description = "绑定 iP")
|
||||
private String bindIp;
|
||||
|
||||
/**
|
||||
* userAgent
|
||||
*/
|
||||
@TableField(value = "bind_user_agent")
|
||||
@Schema(description = "userAgent")
|
||||
private String bindUserAgent;
|
||||
|
||||
/**
|
||||
* 邀请码类型快照:USER=普通用户邀请,AGENT=代理邀请
|
||||
*/
|
||||
@TableField(value = "invite_type")
|
||||
@Schema(description = "邀请码类型快照:USER=普通用户邀请,AGENT=代理邀请")
|
||||
private String inviteType;
|
||||
|
||||
/**
|
||||
* 收益结算归属租户ID(代理结算用,绑定时固化)
|
||||
*/
|
||||
@TableField(value = "profit_tenant_id")
|
||||
@Schema(description = "收益结算归属租户ID(代理结算用,绑定时固化)")
|
||||
private Long profitTenantId;
|
||||
|
||||
/**
|
||||
* 收益归因员工ID(用于区分租户员工/渠道,绑定时固化)
|
||||
*/
|
||||
@TableField(value = "profit_employee_id")
|
||||
@Schema(description = "收益归因员工ID(用于区分租户员工/渠道,绑定时固化)")
|
||||
private Long profitEmployeeId;
|
||||
|
||||
/**
|
||||
* 邀请人所属租户ID快照(便于审计/对账,可选)
|
||||
*/
|
||||
@TableField(value = "inviter_tenant_id")
|
||||
@Schema(description = "邀请人所属租户ID快照(便于审计/对账,可选)")
|
||||
private Long inviterTenantId;
|
||||
|
||||
/**
|
||||
* 邀请码字符串快照(便于排查,可选)
|
||||
*/
|
||||
@TableField(value = "invite_code")
|
||||
@Schema(description = "邀请码字符串快照(便于排查,可选)")
|
||||
private String inviteCode;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.yolo.keyborad.typehandler.StringArrayTypeHandler;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/12 15:16
|
||||
*/
|
||||
|
||||
@Schema
|
||||
@Data
|
||||
@TableName(value = "keyboard_user_purchase_records")
|
||||
public class KeyboardUserPurchaseRecords {
|
||||
/**
|
||||
* 主键,自增,唯一标识每条购买记录
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="主键,自增,唯一标识每条购买记录")
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 用户 ID,关联到用户表,表示是哪位用户购买了产品
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description="用户 ID,关联到用户表,表示是哪位用户购买了产品")
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 购买的产品 ID,关联到产品表
|
||||
*/
|
||||
@TableField(value = "product_id")
|
||||
@Schema(description="购买的产品 ID,关联到产品表")
|
||||
private String productId;
|
||||
|
||||
/**
|
||||
* 购买数量(如内购的金币数量,订阅的时长)
|
||||
*/
|
||||
@TableField(value = "purchase_quantity")
|
||||
@Schema(description="购买数量(如内购的金币数量,订阅的时长)")
|
||||
private Integer purchaseQuantity;
|
||||
|
||||
/**
|
||||
* 实际支付价格
|
||||
*/
|
||||
@TableField(value = "price")
|
||||
@Schema(description="实际支付价格")
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 货币类型(如美元 $)
|
||||
*/
|
||||
@TableField(value = "currency")
|
||||
@Schema(description="货币类型(如美元 $)")
|
||||
private String currency;
|
||||
|
||||
/**
|
||||
* 购买时间
|
||||
*/
|
||||
@TableField(value = "purchase_time")
|
||||
@Schema(description="购买时间")
|
||||
private Date purchaseTime;
|
||||
|
||||
/**
|
||||
* 购买类型(如内购,订阅)
|
||||
*/
|
||||
@TableField(value = "purchase_type")
|
||||
@Schema(description="购买类型(如内购,订阅)")
|
||||
private String purchaseType;
|
||||
|
||||
/**
|
||||
* 购买状态(如已支付,待支付,退款)
|
||||
*/
|
||||
@TableField(value = "\"status\"")
|
||||
@Schema(description="购买状态(如已支付,待支付,退款)")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 支付方式(如信用卡,支付宝等)
|
||||
*/
|
||||
@TableField(value = "payment_method")
|
||||
@Schema(description="支付方式(如信用卡,支付宝等)")
|
||||
private String paymentMethod;
|
||||
|
||||
/**
|
||||
* 唯一的交易 ID,用于标识该购买操作
|
||||
*/
|
||||
@TableField(value = "transaction_id")
|
||||
@Schema(description="唯一的交易 ID,用于标识该购买操作")
|
||||
private String transactionId;
|
||||
|
||||
/**
|
||||
* 苹果的原始交易 ID
|
||||
*/
|
||||
@TableField(value = "original_transaction_id")
|
||||
@Schema(description="苹果的原始交易 ID")
|
||||
private String originalTransactionId;
|
||||
|
||||
/**
|
||||
* 购买的产品 ID 列表(JSON 格式或数组)
|
||||
*/
|
||||
@TableField(value = "product_ids", typeHandler = StringArrayTypeHandler.class, jdbcType = JdbcType.ARRAY)
|
||||
@Schema(description="购买的产品 ID 列表(JSON 格式或数组)")
|
||||
private String[] productIds;
|
||||
|
||||
/**
|
||||
* 苹果返回的购买时间
|
||||
*/
|
||||
@TableField(value = "purchase_date")
|
||||
@Schema(description="苹果返回的购买时间")
|
||||
private Date purchaseDate;
|
||||
|
||||
/**
|
||||
* 苹果返回的过期时间(如果有)
|
||||
*/
|
||||
@TableField(value = "expires_date")
|
||||
@Schema(description="苹果返回的过期时间(如果有)")
|
||||
private Date expiresDate;
|
||||
|
||||
/**
|
||||
* 苹果的环境(如 Sandbox 或 Production)
|
||||
*/
|
||||
@TableField(value = "environment")
|
||||
@Schema(description="苹果的环境(如 Sandbox 或 Production)")
|
||||
private String environment;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/16 16:00
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户免费功能永久总次数额度表(所有功能共用)
|
||||
*/
|
||||
@Schema(description="用户免费功能永久总次数额度表(所有功能共用)")
|
||||
@Data
|
||||
@TableName(value = "keyboard_user_quota_total")
|
||||
public class KeyboardUserQuotaTotal {
|
||||
/**
|
||||
* 用户唯一ID,对应系统用户
|
||||
*/
|
||||
@TableId(value = "user_id", type = IdType.AUTO)
|
||||
@Schema(description="用户唯一ID,对应系统用户")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 免费体验的永久总次数上限(可通过运营活动增加)
|
||||
*/
|
||||
@TableField(value = "total_quota")
|
||||
@Schema(description="免费体验的永久总次数上限(可通过运营活动增加)")
|
||||
private Integer totalQuota;
|
||||
|
||||
/**
|
||||
* 已消耗的免费次数
|
||||
*/
|
||||
@TableField(value = "used_quota")
|
||||
@Schema(description="已消耗的免费次数")
|
||||
private Integer usedQuota;
|
||||
|
||||
/**
|
||||
* 乐观锁版本号(并发控制预留字段)
|
||||
*/
|
||||
@TableField(value = "version")
|
||||
@Schema(description="乐观锁版本号(并发控制预留字段)")
|
||||
private Integer version;
|
||||
|
||||
/**
|
||||
* 首次创建额度记录的时间(通常为注册时间)
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="首次创建额度记录的时间(通常为注册时间)")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 最近一次额度发生变化的时间(消耗或赠送)
|
||||
*/
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description="最近一次额度发生变化的时间(消耗或赠送)")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -11,9 +11,9 @@ import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/10 18:54
|
||||
* @date: 2025/12/22 18:10
|
||||
*/
|
||||
|
||||
|
||||
@Schema
|
||||
@Data
|
||||
@TableName(value = "keyboard_wallet_transaction")
|
||||
@@ -22,62 +22,62 @@ public class KeyboardWalletTransaction {
|
||||
* 主键 Id
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description="主键 Id")
|
||||
@Schema(description = "主键 Id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户 Id
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description="用户 Id")
|
||||
@Schema(description = "用户 Id")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 订单 Id
|
||||
*/
|
||||
@TableField(value = "order_id")
|
||||
@Schema(description="订单 Id")
|
||||
@Schema(description = "订单 Id")
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 金额
|
||||
*/
|
||||
@TableField(value = "amount")
|
||||
@Schema(description="金额")
|
||||
@Schema(description = "金额")
|
||||
private BigDecimal amount;
|
||||
|
||||
/**
|
||||
* 变动类型
|
||||
*/
|
||||
@TableField(value = "\"type\"")
|
||||
@Schema(description="变动类型")
|
||||
@Schema(description = "变动类型")
|
||||
private Short type;
|
||||
|
||||
/**
|
||||
* 变动前余额
|
||||
*/
|
||||
@TableField(value = "before_balance")
|
||||
@Schema(description="变动前余额")
|
||||
@Schema(description = "变动前余额")
|
||||
private BigDecimal beforeBalance;
|
||||
|
||||
/**
|
||||
* 变动后余额
|
||||
*/
|
||||
@TableField(value = "after_balance")
|
||||
@Schema(description="变动后余额")
|
||||
@Schema(description = "变动后余额")
|
||||
private BigDecimal afterBalance;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@TableField(value = "description")
|
||||
@Schema(description="描述")
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description="创建时间")
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.yolo.keyborad.model.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 帖子审核状态枚举
|
||||
*
|
||||
* @author yupi
|
||||
*/
|
||||
public enum PostReviewStatusEnum {
|
||||
|
||||
REVIEWING("待审核", 0),
|
||||
PASS("通过", 1),
|
||||
REJECT("拒绝", 2);
|
||||
|
||||
private final String text;
|
||||
|
||||
private final int value;
|
||||
|
||||
PostReviewStatusEnum(String text, int value) {
|
||||
this.text = text;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getValues() {
|
||||
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
69
src/main/java/com/yolo/keyborad/model/vo/AiCompanionVO.java
Normal file
69
src/main/java/com/yolo/keyborad/model/vo/AiCompanionVO.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "AI陪聊角色VO")
|
||||
public class AiCompanionVO {
|
||||
|
||||
@Schema(description = "陪聊角色唯一ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "角色名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "角色头像URL")
|
||||
private String avatarUrl;
|
||||
|
||||
@Schema(description = "角色封面图URL")
|
||||
private String coverImageUrl;
|
||||
|
||||
@Schema(description = "角色性别(male / female / other)")
|
||||
private String gender;
|
||||
|
||||
@Schema(description = "角色年龄段描述")
|
||||
private String ageRange;
|
||||
|
||||
@Schema(description = "一句话人设描述")
|
||||
private String shortDesc;
|
||||
|
||||
@Schema(description = "角色详细介绍文案")
|
||||
private String introText;
|
||||
|
||||
@Schema(description = "角色性格标签数组")
|
||||
private String personalityTags;
|
||||
|
||||
@Schema(description = "角色说话风格")
|
||||
private String speakingStyle;
|
||||
|
||||
@Schema(description = "排序权重")
|
||||
private Integer sortOrder;
|
||||
|
||||
@Schema(description = "角色热度评分")
|
||||
private Integer popularityScore;
|
||||
|
||||
@Schema(description = "开场白")
|
||||
private String prologue;
|
||||
|
||||
@Schema(description = "开场白音频")
|
||||
private String prologueAudio;
|
||||
|
||||
@Schema(description = "点赞总数")
|
||||
private Integer likeCount;
|
||||
|
||||
@Schema(description = "评论总数")
|
||||
private Integer commentCount;
|
||||
|
||||
@Schema(description = "当前用户是否已点赞")
|
||||
private Boolean liked;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
}
|
||||
37
src/main/java/com/yolo/keyborad/model/vo/AudioTaskVO.java
Normal file
37
src/main/java/com/yolo/keyborad/model/vo/AudioTaskVO.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 音频任务状态
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "音频任务状态")
|
||||
public class AudioTaskVO {
|
||||
|
||||
@Schema(description = "音频任务 ID")
|
||||
private String audioId;
|
||||
|
||||
@Schema(description = "任务状态: pending/processing/completed/failed")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "音频 URL (completed 时返回)")
|
||||
private String audioUrl;
|
||||
|
||||
@Schema(description = "错误信息 (failed 时返回)")
|
||||
private String errorMessage;
|
||||
|
||||
public static final String STATUS_PENDING = "pending";
|
||||
public static final String STATUS_PROCESSING = "processing";
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
public static final String STATUS_FAILED = "failed";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "聊天消息VO")
|
||||
public class ChatMessageHistoryVO {
|
||||
|
||||
@Schema(description = "消息ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "消息发送方:1=用户,2=AI")
|
||||
private Short sender;
|
||||
|
||||
@Schema(description = "聊天消息内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "消息创建时间")
|
||||
private Date createdAt;
|
||||
}
|
||||
29
src/main/java/com/yolo/keyborad/model/vo/ChatMessageVO.java
Normal file
29
src/main/java/com/yolo/keyborad/model/vo/ChatMessageVO.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 消息响应(含异步音频)
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "消息响应")
|
||||
public class ChatMessageVO {
|
||||
|
||||
@Schema(description = "AI 响应文本")
|
||||
private String aiResponse;
|
||||
|
||||
@Schema(description = "音频任务 ID,用于查询音频状态")
|
||||
private String audioId;
|
||||
|
||||
@Schema(description = "LLM 耗时(毫秒)")
|
||||
private Long llmDuration;
|
||||
}
|
||||
29
src/main/java/com/yolo/keyborad/model/vo/ChatSessionVO.java
Normal file
29
src/main/java/com/yolo/keyborad/model/vo/ChatSessionVO.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/28
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "会话信息VO")
|
||||
public class ChatSessionVO {
|
||||
|
||||
@Schema(description = "会话ID")
|
||||
private Long sessionId;
|
||||
|
||||
@Schema(description = "AI陪聊角色ID")
|
||||
private Long companionId;
|
||||
|
||||
@Schema(description = "会话版本号")
|
||||
private Integer resetVersion;
|
||||
|
||||
@Schema(description = "会话创建时间")
|
||||
private Date createdAt;
|
||||
}
|
||||
32
src/main/java/com/yolo/keyborad/model/vo/ChatVoiceVO.java
Normal file
32
src/main/java/com/yolo/keyborad/model/vo/ChatVoiceVO.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 语音对话响应
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "语音对话响应")
|
||||
public class ChatVoiceVO {
|
||||
|
||||
@Schema(description = "用户输入内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "AI 响应文本")
|
||||
private String aiResponse;
|
||||
|
||||
@Schema(description = "AI 语音音频 URL (R2)")
|
||||
private String audioUrl;
|
||||
|
||||
@Schema(description = "处理耗时(毫秒)")
|
||||
private Long duration;
|
||||
}
|
||||
55
src/main/java/com/yolo/keyborad/model/vo/CommentVO.java
Normal file
55
src/main/java/com/yolo/keyborad/model/vo/CommentVO.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/1/26
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "评论VO")
|
||||
public class CommentVO {
|
||||
|
||||
@Schema(description = "评论ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "被评论的AI陪伴角色ID")
|
||||
private Long companionId;
|
||||
|
||||
@Schema(description = "发表评论的用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户昵称")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "用户头像")
|
||||
private String userAvatar;
|
||||
|
||||
@Schema(description = "父评论ID")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "根评论ID")
|
||||
private Long rootId;
|
||||
|
||||
@Schema(description = "评论内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "点赞数")
|
||||
private Integer likeCount;
|
||||
|
||||
@Schema(description = "当前用户是否已点赞")
|
||||
private Boolean liked;
|
||||
|
||||
@Schema(description = "评论创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "回复列表(仅一级评论有值,默认返回前3条)")
|
||||
private List<CommentVO> replies;
|
||||
|
||||
@Schema(description = "回复总数")
|
||||
private Integer replyCount;
|
||||
}
|
||||
32
src/main/java/com/yolo/keyborad/model/vo/SpeechToTextVO.java
Normal file
32
src/main/java/com/yolo/keyborad/model/vo/SpeechToTextVO.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 语音转文字响应VO
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "语音转文字响应")
|
||||
public class SpeechToTextVO {
|
||||
|
||||
@Schema(description = "转录文本")
|
||||
private String transcript;
|
||||
|
||||
@Schema(description = "置信度")
|
||||
private Double confidence;
|
||||
|
||||
@Schema(description = "音频时长(秒)")
|
||||
private Double duration;
|
||||
|
||||
@Schema(description = "检测到的语言")
|
||||
private String detectedLanguage;
|
||||
}
|
||||
26
src/main/java/com/yolo/keyborad/model/vo/TextToSpeechVO.java
Normal file
26
src/main/java/com/yolo/keyborad/model/vo/TextToSpeechVO.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* TTS 语音合成结果
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "TTS 语音合成结果")
|
||||
public class TextToSpeechVO {
|
||||
|
||||
@Schema(description = "音频 Base64")
|
||||
private String audioBase64;
|
||||
|
||||
@Schema(description = "音频 URL (R2)")
|
||||
private String audioUrl;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.yolo.keyborad.model.vo.products;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 商品明细返回 VO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "商品明细返回对象")
|
||||
public class KeyboardProductItemRespVO {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "产品标识符,如 com.loveKey.nyx.2month")
|
||||
private String productId;
|
||||
|
||||
@Schema(description = "产品类型:subscription / in-app-purchase")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "产品名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "产品单位")
|
||||
private String unit;
|
||||
|
||||
@Schema(description = "订阅时长数值")
|
||||
private Integer durationValue;
|
||||
|
||||
@Schema(description = "订阅时长单位")
|
||||
private String durationUnit;
|
||||
|
||||
@Schema(description = "订阅时长天数")
|
||||
private Integer durationDays;
|
||||
|
||||
@Schema(description = "价格")
|
||||
private BigDecimal price;
|
||||
|
||||
@Schema(description = "货币单位")
|
||||
private String currency;
|
||||
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yolo.keyborad.model.vo.user;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 邀请码响应VO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "邀请码信息")
|
||||
public class InviteCodeRespVO {
|
||||
|
||||
@Schema(description = "邀请码")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "邀请码状态:1=启用,0=停用")
|
||||
private Short status;
|
||||
|
||||
@Schema(description = "已使用次数")
|
||||
private Integer usedCount;
|
||||
|
||||
@Schema(description = "最大可使用次数")
|
||||
private Integer maxUses;
|
||||
|
||||
@Schema(description = "过期时间")
|
||||
private Date expiresAt;
|
||||
|
||||
@Schema(description = "H5链接")
|
||||
private String h5Link;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.yolo.keyborad.model.vo.user;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -52,4 +53,8 @@ public class KeyboardUserInfoRespVO {
|
||||
@Schema(description = "VIP 过期时间")
|
||||
private String vipExpiry;
|
||||
|
||||
|
||||
@Schema(description = "vip等级")
|
||||
private Integer vipLevel;
|
||||
|
||||
}
|
||||
@@ -55,4 +55,8 @@ public class KeyboardUserRespVO {
|
||||
*/
|
||||
@Schema(description = "VIP 过期时间")
|
||||
private Date vipExpiry;
|
||||
|
||||
@TableField(value = "vip_level")
|
||||
@Schema(description = "vip等级")
|
||||
private Integer vipLevel;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.yolo.keyborad.model.vo.wallet;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Schema(description = "钱包交易记录响应")
|
||||
public class WalletTransactionRespVO {
|
||||
|
||||
@Schema(description = "交易ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "金额")
|
||||
private BigDecimal amount;
|
||||
|
||||
@Schema(description = "变动类型")
|
||||
private Short type;
|
||||
|
||||
@Schema(description = "变动前余额")
|
||||
private BigDecimal beforeBalance;
|
||||
|
||||
@Schema(description = "变动后余额")
|
||||
private BigDecimal afterBalance;
|
||||
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createdAt;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.yolo.keyborad.service;
|
||||
|
||||
import com.apple.itunes.storekit.model.ResponseBodyV2DecodedPayload;
|
||||
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
|
||||
|
||||
/**
|
||||
* 处理苹果购买后的业务逻辑
|
||||
*/
|
||||
public interface ApplePurchaseService {
|
||||
|
||||
/**
|
||||
* 基于验签结果处理购买逻辑(订阅 / 内购)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param validationResult 苹果验签结果
|
||||
*/
|
||||
void processPurchase(Long userId, AppleReceiptValidationResult validationResult);
|
||||
|
||||
/**
|
||||
* 处理订阅相关通知(新订阅、续订、续订失败、过期等)
|
||||
*
|
||||
* @param notification 解码后的通知载荷
|
||||
*/
|
||||
void handleSubscriptionNotification(ResponseBodyV2DecodedPayload notification);
|
||||
|
||||
/**
|
||||
* 处理退款相关通知(退款、退款拒绝、退款撤销)
|
||||
*
|
||||
* @param notification 解码后的通知载荷
|
||||
*/
|
||||
void handleRefundNotification(ResponseBodyV2DecodedPayload notification);
|
||||
|
||||
/**
|
||||
* 处理续订偏好变更通知
|
||||
*
|
||||
* @param notification 解码后的通知载荷
|
||||
*/
|
||||
void handleRenewalPreferenceChange(ResponseBodyV2DecodedPayload notification);
|
||||
|
||||
/**
|
||||
* 处理消费请求通知
|
||||
*
|
||||
* @param notification 解码后的通知载荷
|
||||
*/
|
||||
void handleConsumptionRequest(ResponseBodyV2DecodedPayload notification);
|
||||
}
|
||||
@@ -5,9 +5,17 @@ import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
|
||||
public interface AppleReceiptService {
|
||||
|
||||
/**
|
||||
* 验证 base64 app receipt 是否有效,并返回解析结果。
|
||||
* 验证 JWS 交易数据是否有效,并返回解析结果。
|
||||
*
|
||||
* @param appReceipt Base64 的 app receipt(以 MI... 开头那串)
|
||||
* @param signedTransaction JWS 格式的签名交易数据
|
||||
*/
|
||||
AppleReceiptValidationResult validateReceipt(String appReceipt);
|
||||
AppleReceiptValidationResult validateReceipt(String signedTransaction);
|
||||
|
||||
/**
|
||||
* 处理 Apple 服务器通知
|
||||
* 验证通知签名并根据通知类型分发到相应的处理逻辑
|
||||
*
|
||||
* @param signedPayload Apple 服务器发送的签名载荷(JWT 格式)
|
||||
*/
|
||||
void processNotification(String signedPayload);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user