实现 IoT SIM 管理模块数据模型和数据库结构
- 添加 IoT 核心业务表:运营商、IoT 卡、设备、号卡、套餐、订单等 - 添加分佣系统表:分佣规则、分佣记录、运营商结算等 - 添加轮询和流量管理表:轮询配置、流量使用记录等 - 添加财务和系统管理表:佣金提现、换卡申请等 - 实现完整的 GORM 模型和常量定义 - 添加数据库迁移脚本和详细文档 - 集成 OpenSpec 工作流工具(opsx 命令和 skills) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
154
.claude/commands/opsx/apply.md
Normal file
154
.claude/commands/opsx/apply.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
---
|
||||||
|
name: OPSX: Apply
|
||||||
|
description: Implement tasks from an OpenSpec change (Experimental)
|
||||||
|
category: Workflow
|
||||||
|
tags: [workflow, artifacts, experimental]
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement tasks from an OpenSpec change.
|
||||||
|
|
||||||
|
**Input**: Optionally specify `--change <name>` after `/opsx:apply`. If omitted, MUST prompt for available changes.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no change name provided, prompt for selection**
|
||||||
|
|
||||||
|
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||||
|
|
||||||
|
Show changes that are implementation-ready (have tasks artifact).
|
||||||
|
Include the schema used for each change if available.
|
||||||
|
Mark changes with incomplete tasks as "(In Progress)".
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
|
2. **Check status to understand the schema**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>" --json
|
||||||
|
```
|
||||||
|
Parse the JSON to understand:
|
||||||
|
- `schemaName`: The workflow being used (e.g., "spec-driven", "tdd")
|
||||||
|
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||||
|
|
||||||
|
3. **Get apply instructions**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openspec instructions apply --change "<name>" --json
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns:
|
||||||
|
- Context file paths (varies by schema)
|
||||||
|
- Progress (total, complete, remaining)
|
||||||
|
- Task list with status
|
||||||
|
- Dynamic instruction based on current state
|
||||||
|
|
||||||
|
**Handle states:**
|
||||||
|
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
||||||
|
- If `state: "all_done"`: congratulate, suggest archive
|
||||||
|
- Otherwise: proceed to implementation
|
||||||
|
|
||||||
|
4. **Read context files**
|
||||||
|
|
||||||
|
Read the files listed in `contextFiles` from the apply instructions output.
|
||||||
|
The files depend on the schema being used:
|
||||||
|
- **spec-driven**: proposal, specs, design, tasks
|
||||||
|
- **tdd**: spec, tests, implementation, docs
|
||||||
|
- Other schemas: follow the contextFiles from CLI output
|
||||||
|
|
||||||
|
5. **Show current progress**
|
||||||
|
|
||||||
|
Display:
|
||||||
|
- Schema being used
|
||||||
|
- Progress: "N/M tasks complete"
|
||||||
|
- Remaining tasks overview
|
||||||
|
- Dynamic instruction from CLI
|
||||||
|
|
||||||
|
6. **Implement tasks (loop until done or blocked)**
|
||||||
|
|
||||||
|
For each pending task:
|
||||||
|
- Show which task is being worked on
|
||||||
|
- Make the code changes required
|
||||||
|
- Keep changes minimal and focused
|
||||||
|
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||||
|
- Continue to next task
|
||||||
|
|
||||||
|
**Pause if:**
|
||||||
|
- Task is unclear → ask for clarification
|
||||||
|
- Implementation reveals a design issue → suggest updating artifacts
|
||||||
|
- Error or blocker encountered → report and wait for guidance
|
||||||
|
- User interrupts
|
||||||
|
|
||||||
|
7. **On completion or pause, show status**
|
||||||
|
|
||||||
|
Display:
|
||||||
|
- Tasks completed this session
|
||||||
|
- Overall progress: "N/M tasks complete"
|
||||||
|
- If all done: suggest archive
|
||||||
|
- If paused: explain why and wait for guidance
|
||||||
|
|
||||||
|
**Output During Implementation**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Implementing: <change-name> (schema: <schema-name>)
|
||||||
|
|
||||||
|
Working on task 3/7: <task description>
|
||||||
|
[...implementation happening...]
|
||||||
|
✓ Task complete
|
||||||
|
|
||||||
|
Working on task 4/7: <task description>
|
||||||
|
[...implementation happening...]
|
||||||
|
✓ Task complete
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output On Completion**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Implementation Complete
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Schema:** <schema-name>
|
||||||
|
**Progress:** 7/7 tasks complete ✓
|
||||||
|
|
||||||
|
### Completed This Session
|
||||||
|
- [x] Task 1
|
||||||
|
- [x] Task 2
|
||||||
|
...
|
||||||
|
|
||||||
|
All tasks complete! Ready to archive this change.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output On Pause (Issue Encountered)**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Implementation Paused
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Schema:** <schema-name>
|
||||||
|
**Progress:** 4/7 tasks complete
|
||||||
|
|
||||||
|
### Issue Encountered
|
||||||
|
<description of the issue>
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
1. <option 1>
|
||||||
|
2. <option 2>
|
||||||
|
3. Other approach
|
||||||
|
|
||||||
|
What would you like to do?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Keep going through tasks until done or blocked
|
||||||
|
- Always read context files before starting (from the apply instructions output)
|
||||||
|
- If task is ambiguous, pause and ask before implementing
|
||||||
|
- If implementation reveals issues, pause and suggest artifact updates
|
||||||
|
- Keep code changes minimal and scoped to each task
|
||||||
|
- Update task checkbox immediately after completing each task
|
||||||
|
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||||
|
- Use contextFiles from CLI output, don't assume specific file names
|
||||||
|
|
||||||
|
**Fluid Workflow Integration**
|
||||||
|
|
||||||
|
This skill supports the "actions on a change" model:
|
||||||
|
|
||||||
|
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||||
|
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||||
175
.claude/commands/opsx/archive.md
Normal file
175
.claude/commands/opsx/archive.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
name: OPSX: Archive
|
||||||
|
description: Archive a completed change in the experimental workflow
|
||||||
|
category: Workflow
|
||||||
|
tags: [workflow, archive, experimental]
|
||||||
|
---
|
||||||
|
|
||||||
|
Archive a completed change in the experimental workflow.
|
||||||
|
|
||||||
|
**Input**: Optionally specify `--change <name>` after `/opsx:archive`. If omitted, MUST prompt for available changes.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no change name provided, prompt for selection**
|
||||||
|
|
||||||
|
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||||
|
|
||||||
|
Show only active changes (not already archived).
|
||||||
|
Include the schema used for each change if available.
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
|
2. **Check artifact completion status**
|
||||||
|
|
||||||
|
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||||
|
|
||||||
|
Parse the JSON to understand:
|
||||||
|
- `schemaName`: The workflow being used
|
||||||
|
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||||
|
|
||||||
|
**If any artifacts are not `done`:**
|
||||||
|
- Display warning listing incomplete artifacts
|
||||||
|
- Prompt user for confirmation to continue
|
||||||
|
- Proceed if user confirms
|
||||||
|
|
||||||
|
3. **Check task completion status**
|
||||||
|
|
||||||
|
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||||
|
|
||||||
|
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||||
|
|
||||||
|
**If incomplete tasks found:**
|
||||||
|
- Display warning showing count of incomplete tasks
|
||||||
|
- Prompt user for confirmation to continue
|
||||||
|
- Proceed if user confirms
|
||||||
|
|
||||||
|
**If no tasks file exists:** Proceed without task-related warning.
|
||||||
|
|
||||||
|
4. **Check if delta specs need syncing**
|
||||||
|
|
||||||
|
Check if `specs/` directory exists in the change with spec files.
|
||||||
|
|
||||||
|
**If delta specs exist, perform a quick sync check:**
|
||||||
|
|
||||||
|
a. **For each delta spec** at `openspec/changes/<name>/specs/<capability>/spec.md`:
|
||||||
|
- Extract requirement names (lines matching `### Requirement: <name>`)
|
||||||
|
- Note which sections exist (ADDED, MODIFIED, REMOVED)
|
||||||
|
|
||||||
|
b. **Check corresponding main spec** at `openspec/specs/<capability>/spec.md`:
|
||||||
|
- If main spec doesn't exist → needs sync
|
||||||
|
- If main spec exists, check if ADDED requirement names appear in it
|
||||||
|
- If any ADDED requirements are missing from main spec → needs sync
|
||||||
|
|
||||||
|
c. **Report findings:**
|
||||||
|
|
||||||
|
**If sync needed:**
|
||||||
|
```
|
||||||
|
⚠️ Delta specs may not be synced:
|
||||||
|
- specs/auth/spec.md → Main spec missing requirement "Token Refresh"
|
||||||
|
- specs/api/spec.md → Main spec doesn't exist yet
|
||||||
|
|
||||||
|
Would you like to sync now before archiving?
|
||||||
|
```
|
||||||
|
- Use **AskUserQuestion tool** with options: "Sync now", "Archive without syncing"
|
||||||
|
- If user chooses sync, execute `/opsx:sync` logic
|
||||||
|
|
||||||
|
**If already synced (all requirements found):**
|
||||||
|
- Proceed without prompting (specs appear to be in sync)
|
||||||
|
|
||||||
|
**If no delta specs exist:** Proceed without sync-related checks.
|
||||||
|
|
||||||
|
5. **Perform the archive**
|
||||||
|
|
||||||
|
Create the archive directory if it doesn't exist:
|
||||||
|
```bash
|
||||||
|
mkdir -p openspec/changes/archive
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||||
|
|
||||||
|
**Check if target already exists:**
|
||||||
|
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||||
|
- If no: Move the change directory to archive
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Display summary**
|
||||||
|
|
||||||
|
Show archive completion summary including:
|
||||||
|
- Change name
|
||||||
|
- Schema that was used
|
||||||
|
- Archive location
|
||||||
|
- Spec sync status (synced / not synced / no delta specs)
|
||||||
|
- Note about any warnings (incomplete artifacts/tasks)
|
||||||
|
|
||||||
|
**Output On Success**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Archive Complete
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Schema:** <schema-name>
|
||||||
|
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||||
|
**Specs:** ✓ Synced to main specs
|
||||||
|
|
||||||
|
All artifacts complete. All tasks complete.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output On Success (No Delta Specs)**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Archive Complete
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Schema:** <schema-name>
|
||||||
|
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||||
|
**Specs:** No delta specs
|
||||||
|
|
||||||
|
All artifacts complete. All tasks complete.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output On Success With Warnings**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Archive Complete (with warnings)
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Schema:** <schema-name>
|
||||||
|
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||||
|
**Specs:** ⚠️ Not synced
|
||||||
|
|
||||||
|
**Warnings:**
|
||||||
|
- Archived with 2 incomplete artifacts
|
||||||
|
- Archived with 3 incomplete tasks
|
||||||
|
- Delta specs were not synced (user chose to skip)
|
||||||
|
|
||||||
|
Review the archive if this was not intentional.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output On Error (Archive Exists)**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Archive Failed
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||||
|
|
||||||
|
Target archive directory already exists.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
1. Rename the existing archive
|
||||||
|
2. Delete the existing archive if it's a duplicate
|
||||||
|
3. Wait until a different date to archive
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Always prompt for change selection if not provided
|
||||||
|
- Use artifact graph (openspec status --json) for completion checking
|
||||||
|
- Don't block archive on warnings - just inform and confirm
|
||||||
|
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||||
|
- Quick sync check: look for requirement names in delta specs, verify they exist in main specs
|
||||||
|
- Show clear summary of what happened
|
||||||
|
- If sync is requested, use /opsx:sync approach (agent-driven)
|
||||||
110
.claude/commands/opsx/continue.md
Normal file
110
.claude/commands/opsx/continue.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
name: OPSX: Continue
|
||||||
|
description: Continue working on a change - create the next artifact (Experimental)
|
||||||
|
category: Workflow
|
||||||
|
tags: [workflow, artifacts, experimental]
|
||||||
|
---
|
||||||
|
|
||||||
|
Continue working on a change by creating the next artifact.
|
||||||
|
|
||||||
|
**Input**: Optionally specify `--change <name>` after `/opsx:continue`. If omitted, MUST prompt for available changes.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no change name provided, prompt for selection**
|
||||||
|
|
||||||
|
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
|
||||||
|
|
||||||
|
Present the top 3-4 most recently modified changes as options, showing:
|
||||||
|
- Change name
|
||||||
|
- Schema (from `schema` field if present, otherwise "spec-driven")
|
||||||
|
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||||
|
- How recently it was modified (from `lastModified` field)
|
||||||
|
|
||||||
|
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
|
2. **Check current status**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>" --json
|
||||||
|
```
|
||||||
|
Parse the JSON to understand current state. The response includes:
|
||||||
|
- `schemaName`: The workflow schema being used (e.g., "spec-driven", "tdd")
|
||||||
|
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||||
|
- `isComplete`: Boolean indicating if all artifacts are complete
|
||||||
|
|
||||||
|
3. **Act based on status**:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**If all artifacts are complete (`isComplete: true`)**:
|
||||||
|
- Congratulate the user
|
||||||
|
- Show final status including the schema used
|
||||||
|
- Suggest: "All artifacts created! You can now implement this change or archive it."
|
||||||
|
- STOP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||||
|
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||||
|
- Get its instructions:
|
||||||
|
```bash
|
||||||
|
openspec instructions <artifact-id> --change "<name>" --json
|
||||||
|
```
|
||||||
|
- Parse the JSON to get template, dependencies, and what it unlocks
|
||||||
|
- **Create the artifact file** using the template as a starting point:
|
||||||
|
- Read any completed dependency files for context
|
||||||
|
- Fill in the template based on context and user's goals
|
||||||
|
- Write to the output path specified in instructions
|
||||||
|
- Show what was created and what's now unlocked
|
||||||
|
- STOP after creating ONE artifact
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**If no artifacts are ready (all blocked)**:
|
||||||
|
- This shouldn't happen with a valid schema
|
||||||
|
- Show status and suggest checking for issues
|
||||||
|
|
||||||
|
4. **After creating an artifact, show progress**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
|
||||||
|
After each invocation, show:
|
||||||
|
- Which artifact was created
|
||||||
|
- Schema workflow being used
|
||||||
|
- Current progress (N/M complete)
|
||||||
|
- What artifacts are now unlocked
|
||||||
|
- Prompt: "Run `/opsx:continue` to create the next artifact"
|
||||||
|
|
||||||
|
**Artifact Creation Guidelines**
|
||||||
|
|
||||||
|
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
|
||||||
|
|
||||||
|
Common artifact patterns:
|
||||||
|
|
||||||
|
**spec-driven schema** (proposal → specs → design → tasks):
|
||||||
|
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||||
|
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||||
|
- **specs/*.md**: Create one spec per capability listed in the proposal.
|
||||||
|
- **design.md**: Document technical decisions, architecture, and implementation approach.
|
||||||
|
- **tasks.md**: Break down implementation into checkboxed tasks.
|
||||||
|
|
||||||
|
**tdd schema** (spec → tests → implementation → docs):
|
||||||
|
- **spec.md**: Feature specification defining what to build.
|
||||||
|
- **tests/*.test.ts**: Write tests BEFORE implementation (TDD red phase).
|
||||||
|
- **src/*.ts**: Implement to make tests pass (TDD green phase).
|
||||||
|
- **docs/*.md**: Document the implemented feature.
|
||||||
|
|
||||||
|
For other schemas, follow the `instruction` field from the CLI output.
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Create ONE artifact per invocation
|
||||||
|
- Always read dependency artifacts before creating a new one
|
||||||
|
- Never skip artifacts or create out of order
|
||||||
|
- If context is unclear, ask the user before creating
|
||||||
|
- Verify the artifact file exists after writing before marking progress
|
||||||
|
- Use the schema's artifact sequence, don't assume specific artifact names
|
||||||
91
.claude/commands/opsx/ff.md
Normal file
91
.claude/commands/opsx/ff.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
name: OPSX: Fast Forward
|
||||||
|
description: Create a change and generate all artifacts needed for implementation in one go
|
||||||
|
category: Workflow
|
||||||
|
tags: [workflow, artifacts, experimental]
|
||||||
|
---
|
||||||
|
|
||||||
|
Fast-forward through artifact creation - generate everything needed to start implementation.
|
||||||
|
|
||||||
|
**Input**: The argument after `/opsx:ff` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no input provided, ask what they want to build**
|
||||||
|
|
||||||
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||||
|
|
||||||
|
2. **Create the change directory**
|
||||||
|
```bash
|
||||||
|
openspec new change "<name>"
|
||||||
|
```
|
||||||
|
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||||
|
|
||||||
|
3. **Get the artifact build order**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>" --json
|
||||||
|
```
|
||||||
|
Parse the JSON to get:
|
||||||
|
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||||
|
- `artifacts`: list of all artifacts with their status and dependencies
|
||||||
|
|
||||||
|
4. **Create artifacts in sequence until apply-ready**
|
||||||
|
|
||||||
|
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||||
|
|
||||||
|
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||||
|
|
||||||
|
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||||
|
- Get instructions:
|
||||||
|
```bash
|
||||||
|
openspec instructions <artifact-id> --change "<name>" --json
|
||||||
|
```
|
||||||
|
- The instructions JSON includes:
|
||||||
|
- `template`: The template content to use
|
||||||
|
- `instruction`: Schema-specific guidance for this artifact type
|
||||||
|
- `outputPath`: Where to write the artifact
|
||||||
|
- `dependencies`: Completed artifacts to read for context
|
||||||
|
- Read any completed dependency files for context
|
||||||
|
- Create the artifact file following the schema's `instruction`
|
||||||
|
- Show brief progress: "✓ Created <artifact-id>"
|
||||||
|
|
||||||
|
b. **Continue until all `applyRequires` artifacts are complete**
|
||||||
|
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||||
|
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||||
|
- Stop when all `applyRequires` artifacts are done
|
||||||
|
|
||||||
|
c. **If an artifact requires user input** (unclear context):
|
||||||
|
- Use **AskUserQuestion tool** to clarify
|
||||||
|
- Then continue with creation
|
||||||
|
|
||||||
|
5. **Show final status**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
|
||||||
|
After completing all artifacts, summarize:
|
||||||
|
- Change name and location
|
||||||
|
- List of artifacts created with brief descriptions
|
||||||
|
- What's ready: "All artifacts created! Ready for implementation."
|
||||||
|
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||||
|
|
||||||
|
**Artifact Creation Guidelines**
|
||||||
|
|
||||||
|
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||||
|
- The schema defines what each artifact should contain - follow it
|
||||||
|
- Read dependency artifacts for context before creating new ones
|
||||||
|
- Use the `template` as a starting point, filling in based on context
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||||
|
- Always read dependency artifacts before creating a new one
|
||||||
|
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||||
|
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||||
|
- Verify each artifact file exists after writing before proceeding to next
|
||||||
69
.claude/commands/opsx/new.md
Normal file
69
.claude/commands/opsx/new.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
name: OPSX: New
|
||||||
|
description: Start a new change using the experimental artifact workflow (OPSX)
|
||||||
|
category: Workflow
|
||||||
|
tags: [workflow, artifacts, experimental]
|
||||||
|
---
|
||||||
|
|
||||||
|
Start a new change using the experimental artifact-driven approach.
|
||||||
|
|
||||||
|
**Input**: The argument after `/opsx:new` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no input provided, ask what they want to build**
|
||||||
|
|
||||||
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||||
|
|
||||||
|
2. **Select a workflow schema**
|
||||||
|
|
||||||
|
Run `openspec schemas --json` to get available schemas with descriptions.
|
||||||
|
|
||||||
|
Use the **AskUserQuestion tool** to let the user choose a workflow:
|
||||||
|
- Present each schema with its description
|
||||||
|
- Mark `spec-driven` as "(default)" if it's available
|
||||||
|
- Example options: "spec-driven - proposal → specs → design → tasks (default)", "tdd - tests → implementation → docs"
|
||||||
|
|
||||||
|
If user doesn't have a preference, default to `spec-driven`.
|
||||||
|
|
||||||
|
3. **Create the change directory**
|
||||||
|
```bash
|
||||||
|
openspec new change "<name>" --schema "<selected-schema>"
|
||||||
|
```
|
||||||
|
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
||||||
|
|
||||||
|
4. **Show the artifact status**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>"
|
||||||
|
```
|
||||||
|
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||||
|
|
||||||
|
5. **Get instructions for the first artifact**
|
||||||
|
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
|
||||||
|
```bash
|
||||||
|
openspec instructions <first-artifact-id> --change "<name>"
|
||||||
|
```
|
||||||
|
This outputs the template and context for creating the first artifact.
|
||||||
|
|
||||||
|
6. **STOP and wait for user direction**
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
|
||||||
|
After completing the steps, summarize:
|
||||||
|
- Change name and location
|
||||||
|
- Selected schema/workflow and its artifact sequence
|
||||||
|
- Current status (0/N artifacts complete)
|
||||||
|
- The template for the first artifact
|
||||||
|
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Do NOT create any artifacts yet - just show the instructions
|
||||||
|
- Do NOT advance beyond showing the first artifact template
|
||||||
|
- If the name is invalid (not kebab-case), ask for a valid name
|
||||||
|
- If a change with that name already exists, suggest using `/opsx:continue` instead
|
||||||
|
- Always pass --schema to preserve the user's workflow choice
|
||||||
134
.claude/commands/opsx/sync.md
Normal file
134
.claude/commands/opsx/sync.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
---
|
||||||
|
name: OPSX: Sync
|
||||||
|
description: Sync delta specs from a change to main specs
|
||||||
|
category: Workflow
|
||||||
|
tags: [workflow, specs, experimental]
|
||||||
|
---
|
||||||
|
|
||||||
|
Sync delta specs from a change to main specs.
|
||||||
|
|
||||||
|
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||||
|
|
||||||
|
**Input**: Optionally specify `--change <name>` after `/opsx:sync`. If omitted, MUST prompt for available changes.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no change name provided, prompt for selection**
|
||||||
|
|
||||||
|
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||||
|
|
||||||
|
Show changes that have delta specs (under `specs/` directory).
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
|
2. **Find delta specs**
|
||||||
|
|
||||||
|
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
|
||||||
|
|
||||||
|
Each delta spec file contains sections like:
|
||||||
|
- `## ADDED Requirements` - New requirements to add
|
||||||
|
- `## MODIFIED Requirements` - Changes to existing requirements
|
||||||
|
- `## REMOVED Requirements` - Requirements to remove
|
||||||
|
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
|
||||||
|
|
||||||
|
If no delta specs found, inform user and stop.
|
||||||
|
|
||||||
|
3. **For each delta spec, apply changes to main specs**
|
||||||
|
|
||||||
|
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
|
||||||
|
|
||||||
|
a. **Read the delta spec** to understand the intended changes
|
||||||
|
|
||||||
|
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
|
||||||
|
|
||||||
|
c. **Apply changes intelligently**:
|
||||||
|
|
||||||
|
**ADDED Requirements:**
|
||||||
|
- If requirement doesn't exist in main spec → add it
|
||||||
|
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||||
|
|
||||||
|
**MODIFIED Requirements:**
|
||||||
|
- Find the requirement in main spec
|
||||||
|
- Apply the changes - this can be:
|
||||||
|
- Adding new scenarios (don't need to copy existing ones)
|
||||||
|
- Modifying existing scenarios
|
||||||
|
- Changing the requirement description
|
||||||
|
- Preserve scenarios/content not mentioned in the delta
|
||||||
|
|
||||||
|
**REMOVED Requirements:**
|
||||||
|
- Remove the entire requirement block from main spec
|
||||||
|
|
||||||
|
**RENAMED Requirements:**
|
||||||
|
- Find the FROM requirement, rename to TO
|
||||||
|
|
||||||
|
d. **Create new main spec** if capability doesn't exist yet:
|
||||||
|
- Create `openspec/specs/<capability>/spec.md`
|
||||||
|
- Add Purpose section (can be brief, mark as TBD)
|
||||||
|
- Add Requirements section with the ADDED requirements
|
||||||
|
|
||||||
|
4. **Show summary**
|
||||||
|
|
||||||
|
After applying all changes, summarize:
|
||||||
|
- Which capabilities were updated
|
||||||
|
- What changes were made (requirements added/modified/removed/renamed)
|
||||||
|
|
||||||
|
**Delta Spec Format Reference**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: New Feature
|
||||||
|
The system SHALL do something new.
|
||||||
|
|
||||||
|
#### Scenario: Basic case
|
||||||
|
- **WHEN** user does X
|
||||||
|
- **THEN** system does Y
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Existing Feature
|
||||||
|
#### Scenario: New scenario to add
|
||||||
|
- **WHEN** user does A
|
||||||
|
- **THEN** system does B
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Deprecated Feature
|
||||||
|
|
||||||
|
## RENAMED Requirements
|
||||||
|
|
||||||
|
- FROM: `### Requirement: Old Name`
|
||||||
|
- TO: `### Requirement: New Name`
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Principle: Intelligent Merging**
|
||||||
|
|
||||||
|
Unlike programmatic merging, you can apply **partial updates**:
|
||||||
|
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||||
|
- The delta represents *intent*, not a wholesale replacement
|
||||||
|
- Use your judgment to merge changes sensibly
|
||||||
|
|
||||||
|
**Output On Success**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Specs Synced: <change-name>
|
||||||
|
|
||||||
|
Updated main specs:
|
||||||
|
|
||||||
|
**<capability-1>**:
|
||||||
|
- Added requirement: "New Feature"
|
||||||
|
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||||
|
|
||||||
|
**<capability-2>**:
|
||||||
|
- Created new spec file
|
||||||
|
- Added requirement: "Another Feature"
|
||||||
|
|
||||||
|
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Read both delta and main specs before making changes
|
||||||
|
- Preserve existing content not mentioned in delta
|
||||||
|
- If something is unclear, ask for clarification
|
||||||
|
- Show what you're changing as you go
|
||||||
|
- The operation should be idempotent - running twice should give same result
|
||||||
152
.claude/skills/openspec-apply-change/SKILL.md
Normal file
152
.claude/skills/openspec-apply-change/SKILL.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
name: openspec-apply-change
|
||||||
|
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement tasks from an OpenSpec change.
|
||||||
|
|
||||||
|
**Input**: Optionally specify a change name. If omitted, MUST prompt for available changes.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no change name provided, prompt for selection**
|
||||||
|
|
||||||
|
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||||
|
|
||||||
|
Show changes that are implementation-ready (have tasks artifact).
|
||||||
|
Include the schema used for each change if available.
|
||||||
|
Mark changes with incomplete tasks as "(In Progress)".
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
|
2. **Check status to understand the schema**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>" --json
|
||||||
|
```
|
||||||
|
Parse the JSON to understand:
|
||||||
|
- `schemaName`: The workflow being used (e.g., "spec-driven", "tdd")
|
||||||
|
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||||
|
|
||||||
|
3. **Get apply instructions**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openspec instructions apply --change "<name>" --json
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns:
|
||||||
|
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
||||||
|
- Progress (total, complete, remaining)
|
||||||
|
- Task list with status
|
||||||
|
- Dynamic instruction based on current state
|
||||||
|
|
||||||
|
**Handle states:**
|
||||||
|
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
||||||
|
- If `state: "all_done"`: congratulate, suggest archive
|
||||||
|
- Otherwise: proceed to implementation
|
||||||
|
|
||||||
|
4. **Read context files**
|
||||||
|
|
||||||
|
Read the files listed in `contextFiles` from the apply instructions output.
|
||||||
|
The files depend on the schema being used:
|
||||||
|
- **spec-driven**: proposal, specs, design, tasks
|
||||||
|
- **tdd**: spec, tests, implementation, docs
|
||||||
|
- Other schemas: follow the contextFiles from CLI output
|
||||||
|
|
||||||
|
5. **Show current progress**
|
||||||
|
|
||||||
|
Display:
|
||||||
|
- Schema being used
|
||||||
|
- Progress: "N/M tasks complete"
|
||||||
|
- Remaining tasks overview
|
||||||
|
- Dynamic instruction from CLI
|
||||||
|
|
||||||
|
6. **Implement tasks (loop until done or blocked)**
|
||||||
|
|
||||||
|
For each pending task:
|
||||||
|
- Show which task is being worked on
|
||||||
|
- Make the code changes required
|
||||||
|
- Keep changes minimal and focused
|
||||||
|
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||||
|
- Continue to next task
|
||||||
|
|
||||||
|
**Pause if:**
|
||||||
|
- Task is unclear → ask for clarification
|
||||||
|
- Implementation reveals a design issue → suggest updating artifacts
|
||||||
|
- Error or blocker encountered → report and wait for guidance
|
||||||
|
- User interrupts
|
||||||
|
|
||||||
|
7. **On completion or pause, show status**
|
||||||
|
|
||||||
|
Display:
|
||||||
|
- Tasks completed this session
|
||||||
|
- Overall progress: "N/M tasks complete"
|
||||||
|
- If all done: suggest archive
|
||||||
|
- If paused: explain why and wait for guidance
|
||||||
|
|
||||||
|
**Output During Implementation**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Implementing: <change-name> (schema: <schema-name>)
|
||||||
|
|
||||||
|
Working on task 3/7: <task description>
|
||||||
|
[...implementation happening...]
|
||||||
|
✓ Task complete
|
||||||
|
|
||||||
|
Working on task 4/7: <task description>
|
||||||
|
[...implementation happening...]
|
||||||
|
✓ Task complete
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output On Completion**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Implementation Complete
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Schema:** <schema-name>
|
||||||
|
**Progress:** 7/7 tasks complete ✓
|
||||||
|
|
||||||
|
### Completed This Session
|
||||||
|
- [x] Task 1
|
||||||
|
- [x] Task 2
|
||||||
|
...
|
||||||
|
|
||||||
|
All tasks complete! Ready to archive this change.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output On Pause (Issue Encountered)**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Implementation Paused
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Schema:** <schema-name>
|
||||||
|
**Progress:** 4/7 tasks complete
|
||||||
|
|
||||||
|
### Issue Encountered
|
||||||
|
<description of the issue>
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
1. <option 1>
|
||||||
|
2. <option 2>
|
||||||
|
3. Other approach
|
||||||
|
|
||||||
|
What would you like to do?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Keep going through tasks until done or blocked
|
||||||
|
- Always read context files before starting (from the apply instructions output)
|
||||||
|
- If task is ambiguous, pause and ask before implementing
|
||||||
|
- If implementation reveals issues, pause and suggest artifact updates
|
||||||
|
- Keep code changes minimal and scoped to each task
|
||||||
|
- Update task checkbox immediately after completing each task
|
||||||
|
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||||
|
- Use contextFiles from CLI output, don't assume specific file names
|
||||||
|
|
||||||
|
**Fluid Workflow Integration**
|
||||||
|
|
||||||
|
This skill supports the "actions on a change" model:
|
||||||
|
|
||||||
|
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||||
|
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||||
126
.claude/skills/openspec-archive-change/SKILL.md
Normal file
126
.claude/skills/openspec-archive-change/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: openspec-archive-change
|
||||||
|
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
||||||
|
---
|
||||||
|
|
||||||
|
Archive a completed change in the experimental workflow.
|
||||||
|
|
||||||
|
**Input**: Optionally specify a change name. If omitted, MUST prompt for available changes.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no change name provided, prompt for selection**
|
||||||
|
|
||||||
|
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||||
|
|
||||||
|
Show only active changes (not already archived).
|
||||||
|
Include the schema used for each change if available.
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
|
2. **Check artifact completion status**
|
||||||
|
|
||||||
|
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||||
|
|
||||||
|
Parse the JSON to understand:
|
||||||
|
- `schemaName`: The workflow being used
|
||||||
|
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||||
|
|
||||||
|
**If any artifacts are not `done`:**
|
||||||
|
- Display warning listing incomplete artifacts
|
||||||
|
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||||
|
- Proceed if user confirms
|
||||||
|
|
||||||
|
3. **Check task completion status**
|
||||||
|
|
||||||
|
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||||
|
|
||||||
|
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||||
|
|
||||||
|
**If incomplete tasks found:**
|
||||||
|
- Display warning showing count of incomplete tasks
|
||||||
|
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||||
|
- Proceed if user confirms
|
||||||
|
|
||||||
|
**If no tasks file exists:** Proceed without task-related warning.
|
||||||
|
|
||||||
|
4. **Check if delta specs need syncing**
|
||||||
|
|
||||||
|
Check if `specs/` directory exists in the change with spec files.
|
||||||
|
|
||||||
|
**If delta specs exist, perform a quick sync check:**
|
||||||
|
|
||||||
|
a. **For each delta spec** at `openspec/changes/<name>/specs/<capability>/spec.md`:
|
||||||
|
- Extract requirement names (lines matching `### Requirement: <name>`)
|
||||||
|
- Note which sections exist (ADDED, MODIFIED, REMOVED)
|
||||||
|
|
||||||
|
b. **Check corresponding main spec** at `openspec/specs/<capability>/spec.md`:
|
||||||
|
- If main spec doesn't exist → needs sync
|
||||||
|
- If main spec exists, check if ADDED requirement names appear in it
|
||||||
|
- If any ADDED requirements are missing from main spec → needs sync
|
||||||
|
|
||||||
|
c. **Report findings:**
|
||||||
|
|
||||||
|
**If sync needed:**
|
||||||
|
```
|
||||||
|
⚠️ Delta specs may not be synced:
|
||||||
|
- specs/auth/spec.md → Main spec missing requirement "Token Refresh"
|
||||||
|
- specs/api/spec.md → Main spec doesn't exist yet
|
||||||
|
|
||||||
|
Would you like to sync now before archiving?
|
||||||
|
```
|
||||||
|
- Use **AskUserQuestion tool** with options: "Sync now", "Archive without syncing"
|
||||||
|
- If user chooses sync, execute /opsx:sync logic (use the openspec-sync-specs skill)
|
||||||
|
|
||||||
|
**If already synced (all requirements found):**
|
||||||
|
- Proceed without prompting (specs appear to be in sync)
|
||||||
|
|
||||||
|
**If no delta specs exist:** Proceed without sync-related checks.
|
||||||
|
|
||||||
|
5. **Perform the archive**
|
||||||
|
|
||||||
|
Create the archive directory if it doesn't exist:
|
||||||
|
```bash
|
||||||
|
mkdir -p openspec/changes/archive
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||||
|
|
||||||
|
**Check if target already exists:**
|
||||||
|
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||||
|
- If no: Move the change directory to archive
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Display summary**
|
||||||
|
|
||||||
|
Show archive completion summary including:
|
||||||
|
- Change name
|
||||||
|
- Schema that was used
|
||||||
|
- Archive location
|
||||||
|
- Whether specs were synced (if applicable)
|
||||||
|
- Note about any warnings (incomplete artifacts/tasks)
|
||||||
|
|
||||||
|
**Output On Success**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Archive Complete
|
||||||
|
|
||||||
|
**Change:** <change-name>
|
||||||
|
**Schema:** <schema-name>
|
||||||
|
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||||
|
**Specs:** ✓ Synced to main specs (or "No delta specs" or "⚠️ Not synced")
|
||||||
|
|
||||||
|
All artifacts complete. All tasks complete.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Always prompt for change selection if not provided
|
||||||
|
- Use artifact graph (openspec status --json) for completion checking
|
||||||
|
- Don't block archive on warnings - just inform and confirm
|
||||||
|
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||||
|
- Show clear summary of what happened
|
||||||
|
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
||||||
|
- Quick sync check: look for requirement names in delta specs, verify they exist in main specs
|
||||||
108
.claude/skills/openspec-continue-change/SKILL.md
Normal file
108
.claude/skills/openspec-continue-change/SKILL.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
name: openspec-continue-change
|
||||||
|
description: Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.
|
||||||
|
---
|
||||||
|
|
||||||
|
Continue working on a change by creating the next artifact.
|
||||||
|
|
||||||
|
**Input**: Optionally specify a change name. If omitted, MUST prompt for available changes.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no change name provided, prompt for selection**
|
||||||
|
|
||||||
|
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
|
||||||
|
|
||||||
|
Present the top 3-4 most recently modified changes as options, showing:
|
||||||
|
- Change name
|
||||||
|
- Schema (from `schema` field if present, otherwise "spec-driven")
|
||||||
|
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||||
|
- How recently it was modified (from `lastModified` field)
|
||||||
|
|
||||||
|
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
|
2. **Check current status**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>" --json
|
||||||
|
```
|
||||||
|
Parse the JSON to understand current state. The response includes:
|
||||||
|
- `schemaName`: The workflow schema being used (e.g., "spec-driven", "tdd")
|
||||||
|
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||||
|
- `isComplete`: Boolean indicating if all artifacts are complete
|
||||||
|
|
||||||
|
3. **Act based on status**:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**If all artifacts are complete (`isComplete: true`)**:
|
||||||
|
- Congratulate the user
|
||||||
|
- Show final status including the schema used
|
||||||
|
- Suggest: "All artifacts created! You can now implement this change or archive it."
|
||||||
|
- STOP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||||
|
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||||
|
- Get its instructions:
|
||||||
|
```bash
|
||||||
|
openspec instructions <artifact-id> --change "<name>" --json
|
||||||
|
```
|
||||||
|
- Parse the JSON to get template, dependencies, and what it unlocks
|
||||||
|
- **Create the artifact file** using the template as a starting point:
|
||||||
|
- Read any completed dependency files for context
|
||||||
|
- Fill in the template based on context and user's goals
|
||||||
|
- Write to the output path specified in instructions
|
||||||
|
- Show what was created and what's now unlocked
|
||||||
|
- STOP after creating ONE artifact
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**If no artifacts are ready (all blocked)**:
|
||||||
|
- This shouldn't happen with a valid schema
|
||||||
|
- Show status and suggest checking for issues
|
||||||
|
|
||||||
|
4. **After creating an artifact, show progress**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
|
||||||
|
After each invocation, show:
|
||||||
|
- Which artifact was created
|
||||||
|
- Schema workflow being used
|
||||||
|
- Current progress (N/M complete)
|
||||||
|
- What artifacts are now unlocked
|
||||||
|
- Prompt: "Want to continue? Just ask me to continue or tell me what to do next."
|
||||||
|
|
||||||
|
**Artifact Creation Guidelines**
|
||||||
|
|
||||||
|
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
|
||||||
|
|
||||||
|
Common artifact patterns:
|
||||||
|
|
||||||
|
**spec-driven schema** (proposal → specs → design → tasks):
|
||||||
|
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||||
|
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||||
|
- **specs/*.md**: Create one spec per capability listed in the proposal.
|
||||||
|
- **design.md**: Document technical decisions, architecture, and implementation approach.
|
||||||
|
- **tasks.md**: Break down implementation into checkboxed tasks.
|
||||||
|
|
||||||
|
**tdd schema** (spec → tests → implementation → docs):
|
||||||
|
- **spec.md**: Feature specification defining what to build.
|
||||||
|
- **tests/*.test.ts**: Write tests BEFORE implementation (TDD red phase).
|
||||||
|
- **src/*.ts**: Implement to make tests pass (TDD green phase).
|
||||||
|
- **docs/*.md**: Document the implemented feature.
|
||||||
|
|
||||||
|
For other schemas, follow the `instruction` field from the CLI output.
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Create ONE artifact per invocation
|
||||||
|
- Always read dependency artifacts before creating a new one
|
||||||
|
- Never skip artifacts or create out of order
|
||||||
|
- If context is unclear, ask the user before creating
|
||||||
|
- Verify the artifact file exists after writing before marking progress
|
||||||
|
- Use the schema's artifact sequence, don't assume specific artifact names
|
||||||
89
.claude/skills/openspec-ff-change/SKILL.md
Normal file
89
.claude/skills/openspec-ff-change/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
name: openspec-ff-change
|
||||||
|
description: Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually.
|
||||||
|
---
|
||||||
|
|
||||||
|
Fast-forward through artifact creation - generate everything needed to start implementation in one go.
|
||||||
|
|
||||||
|
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no clear input provided, ask what they want to build**
|
||||||
|
|
||||||
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||||
|
|
||||||
|
2. **Create the change directory**
|
||||||
|
```bash
|
||||||
|
openspec new change "<name>"
|
||||||
|
```
|
||||||
|
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||||
|
|
||||||
|
3. **Get the artifact build order**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>" --json
|
||||||
|
```
|
||||||
|
Parse the JSON to get:
|
||||||
|
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||||
|
- `artifacts`: list of all artifacts with their status and dependencies
|
||||||
|
|
||||||
|
4. **Create artifacts in sequence until apply-ready**
|
||||||
|
|
||||||
|
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||||
|
|
||||||
|
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||||
|
|
||||||
|
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||||
|
- Get instructions:
|
||||||
|
```bash
|
||||||
|
openspec instructions <artifact-id> --change "<name>" --json
|
||||||
|
```
|
||||||
|
- The instructions JSON includes:
|
||||||
|
- `template`: The template content to use
|
||||||
|
- `instruction`: Schema-specific guidance for this artifact type
|
||||||
|
- `outputPath`: Where to write the artifact
|
||||||
|
- `dependencies`: Completed artifacts to read for context
|
||||||
|
- Read any completed dependency files for context
|
||||||
|
- Create the artifact file following the schema's `instruction`
|
||||||
|
- Show brief progress: "✓ Created <artifact-id>"
|
||||||
|
|
||||||
|
b. **Continue until all `applyRequires` artifacts are complete**
|
||||||
|
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||||
|
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||||
|
- Stop when all `applyRequires` artifacts are done
|
||||||
|
|
||||||
|
c. **If an artifact requires user input** (unclear context):
|
||||||
|
- Use **AskUserQuestion tool** to clarify
|
||||||
|
- Then continue with creation
|
||||||
|
|
||||||
|
5. **Show final status**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
|
||||||
|
After completing all artifacts, summarize:
|
||||||
|
- Change name and location
|
||||||
|
- List of artifacts created with brief descriptions
|
||||||
|
- What's ready: "All artifacts created! Ready for implementation."
|
||||||
|
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||||
|
|
||||||
|
**Artifact Creation Guidelines**
|
||||||
|
|
||||||
|
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||||
|
- The schema defines what each artifact should contain - follow it
|
||||||
|
- Read dependency artifacts for context before creating new ones
|
||||||
|
- Use the `template` as a starting point, filling in based on context
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||||
|
- Always read dependency artifacts before creating a new one
|
||||||
|
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||||
|
- If a change with that name already exists, suggest continuing that change instead
|
||||||
|
- Verify each artifact file exists after writing before proceeding to next
|
||||||
68
.claude/skills/openspec-new-change/SKILL.md
Normal file
68
.claude/skills/openspec-new-change/SKILL.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
name: openspec-new-change
|
||||||
|
description: Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.
|
||||||
|
---
|
||||||
|
|
||||||
|
Start a new change using the experimental artifact-driven approach.
|
||||||
|
|
||||||
|
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no clear input provided, ask what they want to build**
|
||||||
|
|
||||||
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||||
|
|
||||||
|
2. **Select a workflow schema**
|
||||||
|
|
||||||
|
Run `openspec schemas --json` to get available schemas with descriptions.
|
||||||
|
|
||||||
|
Use the **AskUserQuestion tool** to let the user choose a workflow:
|
||||||
|
- Present each schema with its description
|
||||||
|
- Mark `spec-driven` as "(default)" if it's available
|
||||||
|
- Example options: "spec-driven - proposal → specs → design → tasks (default)", "tdd - tests → implementation → docs"
|
||||||
|
|
||||||
|
If user doesn't have a preference, default to `spec-driven`.
|
||||||
|
|
||||||
|
3. **Create the change directory**
|
||||||
|
```bash
|
||||||
|
openspec new change "<name>" --schema "<selected-schema>"
|
||||||
|
```
|
||||||
|
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
||||||
|
|
||||||
|
4. **Show the artifact status**
|
||||||
|
```bash
|
||||||
|
openspec status --change "<name>"
|
||||||
|
```
|
||||||
|
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||||
|
|
||||||
|
5. **Get instructions for the first artifact**
|
||||||
|
The first artifact depends on the schema (e.g., `proposal` for spec-driven, `spec` for tdd).
|
||||||
|
Check the status output to find the first artifact with status "ready".
|
||||||
|
```bash
|
||||||
|
openspec instructions <first-artifact-id> --change "<name>"
|
||||||
|
```
|
||||||
|
This outputs the template and context for creating the first artifact.
|
||||||
|
|
||||||
|
6. **STOP and wait for user direction**
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
|
||||||
|
After completing the steps, summarize:
|
||||||
|
- Change name and location
|
||||||
|
- Selected schema/workflow and its artifact sequence
|
||||||
|
- Current status (0/N artifacts complete)
|
||||||
|
- The template for the first artifact
|
||||||
|
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Do NOT create any artifacts yet - just show the instructions
|
||||||
|
- Do NOT advance beyond showing the first artifact template
|
||||||
|
- If the name is invalid (not kebab-case), ask for a valid name
|
||||||
|
- If a change with that name already exists, suggest continuing that change instead
|
||||||
|
- Always pass --schema to preserve the user's workflow choice
|
||||||
132
.claude/skills/openspec-sync-specs/SKILL.md
Normal file
132
.claude/skills/openspec-sync-specs/SKILL.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
name: openspec-sync-specs
|
||||||
|
description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
|
||||||
|
---
|
||||||
|
|
||||||
|
Sync delta specs from a change to main specs.
|
||||||
|
|
||||||
|
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||||
|
|
||||||
|
**Input**: Optionally specify a change name. If omitted, MUST prompt for available changes.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. **If no change name provided, prompt for selection**
|
||||||
|
|
||||||
|
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||||
|
|
||||||
|
Show changes that have delta specs (under `specs/` directory).
|
||||||
|
|
||||||
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
|
2. **Find delta specs**
|
||||||
|
|
||||||
|
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
|
||||||
|
|
||||||
|
Each delta spec file contains sections like:
|
||||||
|
- `## ADDED Requirements` - New requirements to add
|
||||||
|
- `## MODIFIED Requirements` - Changes to existing requirements
|
||||||
|
- `## REMOVED Requirements` - Requirements to remove
|
||||||
|
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
|
||||||
|
|
||||||
|
If no delta specs found, inform user and stop.
|
||||||
|
|
||||||
|
3. **For each delta spec, apply changes to main specs**
|
||||||
|
|
||||||
|
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
|
||||||
|
|
||||||
|
a. **Read the delta spec** to understand the intended changes
|
||||||
|
|
||||||
|
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
|
||||||
|
|
||||||
|
c. **Apply changes intelligently**:
|
||||||
|
|
||||||
|
**ADDED Requirements:**
|
||||||
|
- If requirement doesn't exist in main spec → add it
|
||||||
|
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||||
|
|
||||||
|
**MODIFIED Requirements:**
|
||||||
|
- Find the requirement in main spec
|
||||||
|
- Apply the changes - this can be:
|
||||||
|
- Adding new scenarios (don't need to copy existing ones)
|
||||||
|
- Modifying existing scenarios
|
||||||
|
- Changing the requirement description
|
||||||
|
- Preserve scenarios/content not mentioned in the delta
|
||||||
|
|
||||||
|
**REMOVED Requirements:**
|
||||||
|
- Remove the entire requirement block from main spec
|
||||||
|
|
||||||
|
**RENAMED Requirements:**
|
||||||
|
- Find the FROM requirement, rename to TO
|
||||||
|
|
||||||
|
d. **Create new main spec** if capability doesn't exist yet:
|
||||||
|
- Create `openspec/specs/<capability>/spec.md`
|
||||||
|
- Add Purpose section (can be brief, mark as TBD)
|
||||||
|
- Add Requirements section with the ADDED requirements
|
||||||
|
|
||||||
|
4. **Show summary**
|
||||||
|
|
||||||
|
After applying all changes, summarize:
|
||||||
|
- Which capabilities were updated
|
||||||
|
- What changes were made (requirements added/modified/removed/renamed)
|
||||||
|
|
||||||
|
**Delta Spec Format Reference**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: New Feature
|
||||||
|
The system SHALL do something new.
|
||||||
|
|
||||||
|
#### Scenario: Basic case
|
||||||
|
- **WHEN** user does X
|
||||||
|
- **THEN** system does Y
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Existing Feature
|
||||||
|
#### Scenario: New scenario to add
|
||||||
|
- **WHEN** user does A
|
||||||
|
- **THEN** system does B
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Deprecated Feature
|
||||||
|
|
||||||
|
## RENAMED Requirements
|
||||||
|
|
||||||
|
- FROM: `### Requirement: Old Name`
|
||||||
|
- TO: `### Requirement: New Name`
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Principle: Intelligent Merging**
|
||||||
|
|
||||||
|
Unlike programmatic merging, you can apply **partial updates**:
|
||||||
|
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||||
|
- The delta represents *intent*, not a wholesale replacement
|
||||||
|
- Use your judgment to merge changes sensibly
|
||||||
|
|
||||||
|
**Output On Success**
|
||||||
|
|
||||||
|
```
|
||||||
|
## Specs Synced: <change-name>
|
||||||
|
|
||||||
|
Updated main specs:
|
||||||
|
|
||||||
|
**<capability-1>**:
|
||||||
|
- Added requirement: "New Feature"
|
||||||
|
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||||
|
|
||||||
|
**<capability-2>**:
|
||||||
|
- Created new spec file
|
||||||
|
- Added requirement: "Another Feature"
|
||||||
|
|
||||||
|
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guardrails**
|
||||||
|
- Read both delta and main specs before making changes
|
||||||
|
- Preserve existing content not mentioned in delta
|
||||||
|
- If something is unclear, ask for clarification
|
||||||
|
- Show what you're changing as you go
|
||||||
|
- The operation should be idempotent - running twice should give same result
|
||||||
940
docs/iot-sim-management/分佣系统说明.md
Normal file
940
docs/iot-sim-management/分佣系统说明.md
Normal file
@@ -0,0 +1,940 @@
|
|||||||
|
# IoT SIM 管理系统 - 分佣系统说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
IoT SIM 管理系统实现了一套灵活的多级代理分佣体系,支持三种分佣模式(一次性分佣、长期分佣、组合分佣),支持阶梯奖励机制,支持自动解冻和手动审批,支持 OR 条件解冻逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分佣架构
|
||||||
|
|
||||||
|
### 多级代理树形结构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 平台(Platform) │
|
||||||
|
│ Level 0 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┼─────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ 一级代理 A │ │ 一级代理 B │ │ 一级代理 C │
|
||||||
|
│ Level 1 │ │ Level 1 │ │ Level 1 │
|
||||||
|
│ Path: /A/ │ │ Path: /B/ │ │ Path: /C/ │
|
||||||
|
└──────────────┘ └──────────────┘ └──────────────┘
|
||||||
|
│ │
|
||||||
|
├────┬────┐ ├────┬────┐
|
||||||
|
▼ ▼ ▼ ▼ ▼ ▼
|
||||||
|
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
|
||||||
|
│A-1 │ │A-2 │ │A-3 │ │B-1 │ │B-2 │ │B-3 │
|
||||||
|
│L2 │ │L2 │ │L2 │ │L2 │ │L2 │ │L2 │
|
||||||
|
└────┘ └────┘ └────┘ └────┘ └────┘ └────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**代理层级关系表**: `agent_hierarchies`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三种分佣模式
|
||||||
|
|
||||||
|
### 1. 一次性分佣 (One-time Commission)
|
||||||
|
|
||||||
|
**特点**: 订单完成后立即发放佣金
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 首次激活奖励
|
||||||
|
- 推广奖励
|
||||||
|
- 快速返佣
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户购买套餐 → 订单完成 → 立即发放佣金给上级代理
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置示例**:
|
||||||
|
```sql
|
||||||
|
INSERT INTO commission_rules (
|
||||||
|
rule_name,
|
||||||
|
rule_type,
|
||||||
|
package_series_id,
|
||||||
|
commission_type,
|
||||||
|
commission_value,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
'一次性分佣-套餐激活奖励',
|
||||||
|
'one_time',
|
||||||
|
1, -- 套餐系列 ID
|
||||||
|
'fixed', -- 固定金额
|
||||||
|
10.00, -- 10元
|
||||||
|
1 -- 启用
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务流程**:
|
||||||
|
```
|
||||||
|
订单创建 → 订单支付 → 订单完成
|
||||||
|
│
|
||||||
|
└─→ 创建分佣记录 (status=1 待发放)
|
||||||
|
│
|
||||||
|
└─→ 自动发放 (status=2 已发放)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 长期分佣 (Long-term Commission)
|
||||||
|
|
||||||
|
**特点**: 订单完成后冻结佣金,满足解冻条件后发放
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 续费奖励
|
||||||
|
- 留存奖励
|
||||||
|
- 长期激励
|
||||||
|
|
||||||
|
**解冻条件**:
|
||||||
|
- **时间条件**: 冻结 N 天后自动解冻
|
||||||
|
- **流量条件**: IoT 卡累计使用 M MB 流量后解冻
|
||||||
|
- **OR 逻辑**: 时间到期 **OR** 流量达标,满足任一条件即可解冻
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户购买套餐 → 订单完成 → 冻结佣金 (30天或1GB流量)
|
||||||
|
↓
|
||||||
|
时间到期 OR 流量达标
|
||||||
|
↓
|
||||||
|
自动解冻发放
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置示例**:
|
||||||
|
```sql
|
||||||
|
INSERT INTO commission_rules (
|
||||||
|
rule_name,
|
||||||
|
rule_type,
|
||||||
|
package_series_id,
|
||||||
|
commission_type,
|
||||||
|
commission_value,
|
||||||
|
freeze_days,
|
||||||
|
freeze_data_mb,
|
||||||
|
unfreeze_mode,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
'长期分佣-续费奖励',
|
||||||
|
'long_term',
|
||||||
|
1,
|
||||||
|
'percentage', -- 百分比
|
||||||
|
0.10, -- 10%
|
||||||
|
30, -- 冻结30天
|
||||||
|
1024, -- 或使用1GB流量
|
||||||
|
'auto', -- 自动解冻
|
||||||
|
1
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务流程**:
|
||||||
|
```
|
||||||
|
订单创建 → 订单支付 → 订单完成
|
||||||
|
│
|
||||||
|
└─→ 创建分佣记录 (status=3 已冻结)
|
||||||
|
│
|
||||||
|
├─→ 时间检查: 30天后 → 自动解冻 (status=2 已发放)
|
||||||
|
│
|
||||||
|
└─→ 流量检查: 使用1GB流量后 → 自动解冻 (status=2 已发放)
|
||||||
|
```
|
||||||
|
|
||||||
|
**解冻条件数据结构**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"time_based": {
|
||||||
|
"days": 30,
|
||||||
|
"deadline": "2025-02-10T00:00:00Z"
|
||||||
|
},
|
||||||
|
"data_based": {
|
||||||
|
"data_mb": 1024,
|
||||||
|
"iot_card_id": 12345
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 组合分佣 (Combined Commission)
|
||||||
|
|
||||||
|
**特点**: 同时包含一次性分佣和长期分佣,订单完成后部分立即发放,部分冻结
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 首充奖励(立即发放) + 留存奖励(冻结发放)
|
||||||
|
- 灵活激励机制
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户购买套餐 → 订单完成 → 立即发放 5元 + 冻结 10元 (30天后发放)
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置示例**:
|
||||||
|
```sql
|
||||||
|
-- 1. 创建组合分佣规则
|
||||||
|
INSERT INTO commission_rules (
|
||||||
|
rule_name,
|
||||||
|
rule_type,
|
||||||
|
package_series_id,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
'组合分佣-首充+留存',
|
||||||
|
'combined',
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 配置一次性条件
|
||||||
|
INSERT INTO commission_combined_conditions (
|
||||||
|
rule_id,
|
||||||
|
condition_type,
|
||||||
|
commission_type,
|
||||||
|
commission_value
|
||||||
|
) VALUES (
|
||||||
|
1, -- 上面创建的规则 ID
|
||||||
|
'one_time',
|
||||||
|
'fixed',
|
||||||
|
5.00 -- 立即发放 5元
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 配置长期条件
|
||||||
|
INSERT INTO commission_combined_conditions (
|
||||||
|
rule_id,
|
||||||
|
condition_type,
|
||||||
|
commission_type,
|
||||||
|
commission_value,
|
||||||
|
freeze_days,
|
||||||
|
freeze_data_mb
|
||||||
|
) VALUES (
|
||||||
|
1,
|
||||||
|
'long_term',
|
||||||
|
'fixed',
|
||||||
|
10.00, -- 冻结 10元
|
||||||
|
30, -- 30天
|
||||||
|
1024 -- 或1GB流量
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务流程**:
|
||||||
|
```
|
||||||
|
订单创建 → 订单支付 → 订单完成
|
||||||
|
│
|
||||||
|
├─→ 创建一次性分佣记录 (status=1 待发放) → 立即发放 (status=2)
|
||||||
|
│
|
||||||
|
└─→ 创建长期分佣记录 (status=3 已冻结) → 满足条件后解冻
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 阶梯奖励机制
|
||||||
|
|
||||||
|
### 阶梯奖励说明
|
||||||
|
|
||||||
|
阶梯奖励允许根据订单数量设置不同的分佣标准,订单数量越多,分佣越高。
|
||||||
|
|
||||||
|
**示例配置**:
|
||||||
|
```
|
||||||
|
1-10 单: 10元/单
|
||||||
|
11-50 单: 15元/单
|
||||||
|
51+ 单: 20元/单
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置示例
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 1. 创建支持阶梯的分佣规则
|
||||||
|
INSERT INTO commission_rules (
|
||||||
|
rule_name,
|
||||||
|
rule_type,
|
||||||
|
package_series_id,
|
||||||
|
enable_ladder,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
'阶梯分佣-月度订单量',
|
||||||
|
'one_time',
|
||||||
|
1,
|
||||||
|
true, -- 启用阶梯
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 配置阶梯奖励
|
||||||
|
INSERT INTO commission_ladder (rule_id, min_quantity, max_quantity, commission_type, commission_value) VALUES
|
||||||
|
(1, 1, 10, 'fixed', 10.00),
|
||||||
|
(1, 11, 50, 'fixed', 15.00),
|
||||||
|
(1, 51, NULL, 'fixed', 20.00); -- NULL 表示无上限
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶梯计算逻辑
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
func CalculateLadderCommission(agentID uint, ruleID uint, currentMonth string) float64 {
|
||||||
|
// 1. 查询阶梯配置
|
||||||
|
ladders := db.FindCommissionLadders(ruleID)
|
||||||
|
|
||||||
|
// 2. 统计当月订单数量
|
||||||
|
orderCount := db.CountOrders(agentID, currentMonth)
|
||||||
|
|
||||||
|
// 3. 匹配阶梯
|
||||||
|
for _, ladder := range ladders {
|
||||||
|
if orderCount >= ladder.MinQuantity &&
|
||||||
|
(ladder.MaxQuantity == nil || orderCount <= ladder.MaxQuantity) {
|
||||||
|
if ladder.CommissionType == "fixed" {
|
||||||
|
return ladder.CommissionValue
|
||||||
|
} else if ladder.CommissionType == "percentage" {
|
||||||
|
orderAmount := db.GetOrderAmount(orderID)
|
||||||
|
return orderAmount * ladder.CommissionValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分佣计算方式
|
||||||
|
|
||||||
|
### 1. 固定金额 (Fixed)
|
||||||
|
|
||||||
|
**说明**: 每笔订单固定分佣 N 元
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```sql
|
||||||
|
commission_type = 'fixed'
|
||||||
|
commission_value = 10.00
|
||||||
|
```
|
||||||
|
|
||||||
|
**计算公式**:
|
||||||
|
```
|
||||||
|
分佣金额 = commission_value = 10.00 元
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 百分比 (Percentage)
|
||||||
|
|
||||||
|
**说明**: 按订单金额的 N% 分佣
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```sql
|
||||||
|
commission_type = 'percentage'
|
||||||
|
commission_value = 0.10 -- 10%
|
||||||
|
```
|
||||||
|
|
||||||
|
**计算公式**:
|
||||||
|
```
|
||||||
|
分佣金额 = 订单金额 × commission_value
|
||||||
|
= 100.00 × 0.10
|
||||||
|
= 10.00 元
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分佣记录表
|
||||||
|
|
||||||
|
### 表结构: `commission_records`
|
||||||
|
|
||||||
|
分佣记录表记录每笔分佣的详细信息:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE commission_records (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
order_id BIGINT NOT NULL,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
rule_id BIGINT NOT NULL,
|
||||||
|
commission_type VARCHAR(50) NOT NULL,
|
||||||
|
commission_amount DECIMAL(10,2) NOT NULL,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
freeze_days INT DEFAULT 0,
|
||||||
|
freeze_data_mb BIGINT DEFAULT 0,
|
||||||
|
unfreeze_conditions JSONB,
|
||||||
|
unfrozen_at TIMESTAMPTZ,
|
||||||
|
distributed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态说明
|
||||||
|
|
||||||
|
| status | 状态 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 1 | 待发放 | 一次性分佣,等待发放 |
|
||||||
|
| 2 | 已发放 | 已发放到代理账户 |
|
||||||
|
| 3 | 已冻结 | 长期分佣,冻结中 |
|
||||||
|
| 4 | 已取消 | 订单取消或退款,分佣取消 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OR 条件解冻逻辑
|
||||||
|
|
||||||
|
### 解冻条件设计
|
||||||
|
|
||||||
|
长期分佣支持 **OR 条件解冻**,即时间到期 **OR** 流量达标,满足任一条件即可自动解冻。
|
||||||
|
|
||||||
|
**解冻条件数据结构**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"time_based": {
|
||||||
|
"days": 30,
|
||||||
|
"deadline": "2025-02-10T00:00:00Z"
|
||||||
|
},
|
||||||
|
"data_based": {
|
||||||
|
"data_mb": 1024,
|
||||||
|
"iot_card_id": 12345
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 解冻检查逻辑
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
func CheckUnfreezeConditions(record *CommissionRecord) bool {
|
||||||
|
var conditions struct {
|
||||||
|
TimeBased struct {
|
||||||
|
Days int `json:"days"`
|
||||||
|
Deadline time.Time `json:"deadline"`
|
||||||
|
} `json:"time_based"`
|
||||||
|
DataBased struct {
|
||||||
|
DataMB int64 `json:"data_mb"`
|
||||||
|
IotCardID uint `json:"iot_card_id"`
|
||||||
|
} `json:"data_based"`
|
||||||
|
}
|
||||||
|
|
||||||
|
json.Unmarshal(record.UnfreezeConditions, &conditions)
|
||||||
|
|
||||||
|
// 检查时间条件
|
||||||
|
if time.Now().After(conditions.TimeBased.Deadline) {
|
||||||
|
return true // 时间到期,可以解冻
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查流量条件
|
||||||
|
if conditions.DataBased.IotCardID > 0 {
|
||||||
|
card := db.FindIotCardByID(conditions.DataBased.IotCardID)
|
||||||
|
if card.DataUsageMB >= conditions.DataBased.DataMB {
|
||||||
|
return true // 流量达标,可以解冻
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false // 条件均未满足
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动解冻定时任务
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
func UnfreezeCommissionTask() {
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
// 查询所有冻结中的分佣记录
|
||||||
|
records := db.FindCommissionRecords("status = 3")
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
if CheckUnfreezeConditions(&record) {
|
||||||
|
// 解冻
|
||||||
|
record.Status = 2 // 已发放
|
||||||
|
record.UnfrozenAt = time.Now()
|
||||||
|
record.DistributedAt = time.Now()
|
||||||
|
db.Save(&record)
|
||||||
|
|
||||||
|
// 发放到代理账户
|
||||||
|
DistributeCommission(record.AgentID, record.CommissionAmount)
|
||||||
|
|
||||||
|
logger.Info("分佣解冻成功",
|
||||||
|
zap.Uint("record_id", record.ID),
|
||||||
|
zap.Uint("agent_id", record.AgentID),
|
||||||
|
zap.Float64("amount", record.CommissionAmount),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分佣审批流程
|
||||||
|
|
||||||
|
### 自动审批 vs 手动审批
|
||||||
|
|
||||||
|
分佣规则的 `unfreeze_mode` 字段控制解冻模式:
|
||||||
|
|
||||||
|
- **auto**: 自动解冻,满足条件后自动发放
|
||||||
|
- **manual**: 手动审批,需要人工审核通过后才能发放
|
||||||
|
|
||||||
|
### 手动审批流程
|
||||||
|
|
||||||
|
```
|
||||||
|
订单完成 → 创建分佣记录 (status=3 已冻结)
|
||||||
|
↓
|
||||||
|
满足解冻条件
|
||||||
|
↓
|
||||||
|
创建审批记录 (approval_status=1 待审批)
|
||||||
|
↓
|
||||||
|
审批人审核
|
||||||
|
├─→ 通过 (approval_status=2) → 发放佣金 (status=2 已发放)
|
||||||
|
└─→ 拒绝 (approval_status=3) → 取消分佣 (status=4 已取消)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 审批表: `commission_approvals`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE commission_approvals (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
commission_record_id BIGINT UNIQUE NOT NULL,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
approval_status INT NOT NULL DEFAULT 1,
|
||||||
|
approver_id BIGINT,
|
||||||
|
approval_reason TEXT,
|
||||||
|
approved_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 审批状态
|
||||||
|
|
||||||
|
| approval_status | 状态 | 说明 |
|
||||||
|
|----------------|------|------|
|
||||||
|
| 1 | 待审批 | 等待审批人审核 |
|
||||||
|
| 2 | 已通过 | 审批通过,发放佣金 |
|
||||||
|
| 3 | 已拒绝 | 审批拒绝,取消分佣 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分佣模板
|
||||||
|
|
||||||
|
### 模板设计
|
||||||
|
|
||||||
|
分佣模板用于快速创建分佣规则,避免重复配置。
|
||||||
|
|
||||||
|
**表结构**: `commission_templates`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE commission_templates (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
template_name VARCHAR(255) NOT NULL,
|
||||||
|
template_data JSONB NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模板数据格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rule_type": "combined",
|
||||||
|
"package_series_id": 1,
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"condition_type": "one_time",
|
||||||
|
"commission_type": "fixed",
|
||||||
|
"commission_value": 5.00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition_type": "long_term",
|
||||||
|
"commission_type": "fixed",
|
||||||
|
"commission_value": 10.00,
|
||||||
|
"freeze_days": 30,
|
||||||
|
"freeze_data_mb": 1024
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用模板创建规则
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
func CreateRuleFromTemplate(templateID uint, seriesID uint) error {
|
||||||
|
template := db.FindTemplateByID(templateID)
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
RuleType string `json:"rule_type"`
|
||||||
|
PackageSeriesID uint `json:"package_series_id"`
|
||||||
|
Conditions []struct {
|
||||||
|
ConditionType string `json:"condition_type"`
|
||||||
|
CommissionType string `json:"commission_type"`
|
||||||
|
CommissionValue float64 `json:"commission_value"`
|
||||||
|
FreezeDays int `json:"freeze_days"`
|
||||||
|
FreezeDataMB int64 `json:"freeze_data_mb"`
|
||||||
|
} `json:"conditions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
json.Unmarshal(template.TemplateData, &data)
|
||||||
|
|
||||||
|
// 创建分佣规则
|
||||||
|
rule := CommissionRule{
|
||||||
|
RuleName: template.TemplateName,
|
||||||
|
RuleType: data.RuleType,
|
||||||
|
PackageSeriesID: seriesID,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
db.Create(&rule)
|
||||||
|
|
||||||
|
// 创建组合条件
|
||||||
|
for _, cond := range data.Conditions {
|
||||||
|
condition := CommissionCombinedCondition{
|
||||||
|
RuleID: rule.ID,
|
||||||
|
ConditionType: cond.ConditionType,
|
||||||
|
CommissionType: cond.CommissionType,
|
||||||
|
CommissionValue: cond.CommissionValue,
|
||||||
|
FreezeDays: cond.FreezeDays,
|
||||||
|
FreezeDataMB: cond.FreezeDataMB,
|
||||||
|
}
|
||||||
|
db.Create(&condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 运营商结算
|
||||||
|
|
||||||
|
### 结算表: `carrier_settlements`
|
||||||
|
|
||||||
|
记录与运营商的月度结算情况:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE carrier_settlements (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
carrier_id BIGINT NOT NULL,
|
||||||
|
settlement_month VARCHAR(7) NOT NULL,
|
||||||
|
total_orders INT DEFAULT 0,
|
||||||
|
total_amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
settlement_status INT NOT NULL DEFAULT 1,
|
||||||
|
settled_at TIMESTAMPTZ,
|
||||||
|
paid_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 结算状态
|
||||||
|
|
||||||
|
| settlement_status | 状态 | 说明 |
|
||||||
|
|------------------|------|------|
|
||||||
|
| 1 | 待结算 | 月度未结束 |
|
||||||
|
| 2 | 已结算 | 已统计金额 |
|
||||||
|
| 3 | 已支付 | 已支付给运营商 |
|
||||||
|
|
||||||
|
### 月度结算流程
|
||||||
|
|
||||||
|
```
|
||||||
|
每月1号 → 统计上月订单数据
|
||||||
|
↓
|
||||||
|
创建结算记录 (settlement_status=1 待结算)
|
||||||
|
↓
|
||||||
|
财务审核
|
||||||
|
↓
|
||||||
|
确认结算 (settlement_status=2 已结算)
|
||||||
|
↓
|
||||||
|
支付运营商 (settlement_status=3 已支付)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 结算计算逻辑
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
func GenerateCarrierSettlement(carrierID uint, month string) error {
|
||||||
|
// 1. 统计上月订单
|
||||||
|
orders := db.FindOrders("carrier_id = ? AND DATE_FORMAT(completed_at, '%Y-%m') = ?", carrierID, month)
|
||||||
|
|
||||||
|
totalOrders := len(orders)
|
||||||
|
totalAmount := 0.0
|
||||||
|
for _, order := range orders {
|
||||||
|
totalAmount += order.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建结算记录
|
||||||
|
settlement := CarrierSettlement{
|
||||||
|
CarrierID: carrierID,
|
||||||
|
SettlementMonth: month,
|
||||||
|
TotalOrders: totalOrders,
|
||||||
|
TotalAmount: totalAmount,
|
||||||
|
SettlementStatus: 1, // 待结算
|
||||||
|
}
|
||||||
|
db.Create(&settlement)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 提现管理
|
||||||
|
|
||||||
|
### 提现申请表: `commission_withdrawal_requests`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE commission_withdrawal_requests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
withdrawal_amount DECIMAL(10,2) NOT NULL,
|
||||||
|
withdrawal_method VARCHAR(20) NOT NULL,
|
||||||
|
account_info JSONB NOT NULL,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
reviewer_id BIGINT,
|
||||||
|
review_reason TEXT,
|
||||||
|
reviewed_at TIMESTAMPTZ,
|
||||||
|
paid_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提现状态
|
||||||
|
|
||||||
|
| status | 状态 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 1 | 待审核 | 等待审核 |
|
||||||
|
| 2 | 已通过 | 审核通过,等待打款 |
|
||||||
|
| 3 | 已拒绝 | 审核拒绝 |
|
||||||
|
| 4 | 已打款 | 已打款到账户 |
|
||||||
|
| 5 | 已取消 | 用户取消 |
|
||||||
|
|
||||||
|
### 提现流程
|
||||||
|
|
||||||
|
```
|
||||||
|
代理提交提现申请 (status=1 待审核)
|
||||||
|
↓
|
||||||
|
财务审核
|
||||||
|
├─→ 通过 (status=2 已通过) → 打款 (status=4 已打款)
|
||||||
|
└─→ 拒绝 (status=3 已拒绝)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提现设置表: `commission_withdrawal_settings`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE commission_withdrawal_settings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT UNIQUE NOT NULL,
|
||||||
|
min_withdrawal_amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
max_withdrawal_amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
withdrawal_fee_rate DECIMAL(5,4) DEFAULT 0,
|
||||||
|
auto_approval_enabled BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提现规则检查
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
func ValidateWithdrawalRequest(agentID uint, amount float64) error {
|
||||||
|
setting := db.FindWithdrawalSetting(agentID)
|
||||||
|
|
||||||
|
// 检查最小金额
|
||||||
|
if amount < setting.MinWithdrawalAmount {
|
||||||
|
return fmt.Errorf("提现金额不能低于 %.2f 元", setting.MinWithdrawalAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查最大金额
|
||||||
|
if setting.MaxWithdrawalAmount > 0 && amount > setting.MaxWithdrawalAmount {
|
||||||
|
return fmt.Errorf("提现金额不能高于 %.2f 元", setting.MaxWithdrawalAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查账户余额
|
||||||
|
balance := db.GetAgentBalance(agentID)
|
||||||
|
fee := amount * setting.WithdrawalFeeRate
|
||||||
|
totalAmount := amount + fee
|
||||||
|
|
||||||
|
if balance < totalAmount {
|
||||||
|
return fmt.Errorf("余额不足,需要 %.2f 元(含手续费 %.2f 元)", totalAmount, fee)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分佣业务流程示例
|
||||||
|
|
||||||
|
### 示例 1: 一次性分佣
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户购买套餐(100元)
|
||||||
|
↓
|
||||||
|
2. 订单完成
|
||||||
|
↓
|
||||||
|
3. 触发分佣计算
|
||||||
|
- 规则: 一次性分佣,固定金额 10元
|
||||||
|
- 创建分佣记录: agent_id=123, commission_amount=10.00, status=1 待发放
|
||||||
|
↓
|
||||||
|
4. 自动发放
|
||||||
|
- 更新分佣记录: status=2 已发放, distributed_at=NOW()
|
||||||
|
- 更新代理账户余额: balance += 10.00
|
||||||
|
↓
|
||||||
|
5. 完成
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 2: 长期分佣(OR 条件解冻)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户购买套餐(100元)
|
||||||
|
↓
|
||||||
|
2. 订单完成
|
||||||
|
↓
|
||||||
|
3. 触发分佣计算
|
||||||
|
- 规则: 长期分佣,10%,冻结30天 OR 使用1GB流量
|
||||||
|
- 创建分佣记录: agent_id=123, commission_amount=10.00, status=3 已冻结
|
||||||
|
- 解冻条件: {"time_based": {"days": 30}, "data_based": {"data_mb": 1024}}
|
||||||
|
↓
|
||||||
|
4. 定时任务检查解冻条件
|
||||||
|
- 时间检查: 30天后 → 满足条件 → 解冻
|
||||||
|
- 流量检查: 使用1GB流量后 → 满足条件 → 解冻
|
||||||
|
↓
|
||||||
|
5. 自动解冻
|
||||||
|
- 更新分佣记录: status=2 已发放, unfrozen_at=NOW(), distributed_at=NOW()
|
||||||
|
- 更新代理账户余额: balance += 10.00
|
||||||
|
↓
|
||||||
|
6. 完成
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 3: 组合分佣
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户购买套餐(100元)
|
||||||
|
↓
|
||||||
|
2. 订单完成
|
||||||
|
↓
|
||||||
|
3. 触发分佣计算
|
||||||
|
- 规则: 组合分佣
|
||||||
|
- 一次性条件: 固定金额 5元
|
||||||
|
- 长期条件: 固定金额 10元,冻结30天
|
||||||
|
↓
|
||||||
|
4. 创建两条分佣记录
|
||||||
|
- 记录1: agent_id=123, commission_amount=5.00, status=1 待发放
|
||||||
|
- 记录2: agent_id=123, commission_amount=10.00, status=3 已冻结
|
||||||
|
↓
|
||||||
|
5. 立即发放一次性分佣
|
||||||
|
- 记录1: status=2 已发放
|
||||||
|
- 代理账户余额: balance += 5.00
|
||||||
|
↓
|
||||||
|
6. 30天后自动解冻长期分佣
|
||||||
|
- 记录2: status=2 已发放
|
||||||
|
- 代理账户余额: balance += 10.00
|
||||||
|
↓
|
||||||
|
7. 完成
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 监控和统计
|
||||||
|
|
||||||
|
### 分佣统计指标
|
||||||
|
|
||||||
|
1. **代理分佣总额**
|
||||||
|
- 待发放金额
|
||||||
|
- 已发放金额
|
||||||
|
- 已冻结金额
|
||||||
|
|
||||||
|
2. **分佣发放效率**
|
||||||
|
- 平均发放时长
|
||||||
|
- 平均解冻时长
|
||||||
|
|
||||||
|
3. **提现统计**
|
||||||
|
- 提现申请数量
|
||||||
|
- 提现成功率
|
||||||
|
- 提现金额统计
|
||||||
|
|
||||||
|
### SQL 查询示例
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 1. 代理分佣总额统计
|
||||||
|
SELECT
|
||||||
|
agent_id,
|
||||||
|
SUM(CASE WHEN status = 1 THEN commission_amount ELSE 0 END) AS pending_amount,
|
||||||
|
SUM(CASE WHEN status = 2 THEN commission_amount ELSE 0 END) AS distributed_amount,
|
||||||
|
SUM(CASE WHEN status = 3 THEN commission_amount ELSE 0 END) AS frozen_amount
|
||||||
|
FROM commission_records
|
||||||
|
WHERE agent_id = 123
|
||||||
|
GROUP BY agent_id;
|
||||||
|
|
||||||
|
-- 2. 月度分佣统计
|
||||||
|
SELECT
|
||||||
|
DATE_FORMAT(created_at, '%Y-%m') AS month,
|
||||||
|
COUNT(*) AS total_records,
|
||||||
|
SUM(commission_amount) AS total_amount
|
||||||
|
FROM commission_records
|
||||||
|
WHERE agent_id = 123
|
||||||
|
AND status = 2
|
||||||
|
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
|
||||||
|
ORDER BY month DESC;
|
||||||
|
|
||||||
|
-- 3. 提现统计
|
||||||
|
SELECT
|
||||||
|
status,
|
||||||
|
COUNT(*) AS request_count,
|
||||||
|
SUM(withdrawal_amount) AS total_amount
|
||||||
|
FROM commission_withdrawal_requests
|
||||||
|
WHERE agent_id = 123
|
||||||
|
GROUP BY status;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 合理设置冻结条件
|
||||||
|
|
||||||
|
- **过短**: 可能导致代理流失
|
||||||
|
- **过长**: 影响代理积极性
|
||||||
|
- **建议**: 根据业务特点和用户留存数据设置合理的冻结期
|
||||||
|
|
||||||
|
### 2. 使用 OR 条件解冻
|
||||||
|
|
||||||
|
- **优势**: 提高解冻灵活性,代理满足任一条件即可获得佣金
|
||||||
|
- **示例**: 30天 OR 1GB流量,满足其一即可解冻
|
||||||
|
|
||||||
|
### 3. 启用阶梯奖励
|
||||||
|
|
||||||
|
- **优势**: 激励代理提高订单量
|
||||||
|
- **示例**: 月订单量越多,单笔佣金越高
|
||||||
|
|
||||||
|
### 4. 定期审查分佣规则
|
||||||
|
|
||||||
|
- 定期分析分佣数据,优化分佣规则
|
||||||
|
- 根据代理反馈调整冻结条件和佣金比例
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
IoT SIM 管理系统的分佣系统具有以下特点:
|
||||||
|
|
||||||
|
1. **三种分佣模式**: 一次性分佣、长期分佣、组合分佣
|
||||||
|
2. **阶梯奖励机制**: 支持根据订单数量设置不同的分佣标准
|
||||||
|
3. **OR 条件解冻**: 时间到期 OR 流量达标,满足任一条件即可解冻
|
||||||
|
4. **自动 + 手动审批**: 支持自动解冻和手动审批两种模式
|
||||||
|
5. **分佣模板**: 快速创建分佣规则,避免重复配置
|
||||||
|
6. **运营商结算**: 记录与运营商的月度结算情况
|
||||||
|
7. **提现管理**: 完善的提现申请和审批流程
|
||||||
|
8. **多级代理**: 支持无限层级的代理树形结构
|
||||||
|
|
||||||
|
通过灵活配置和使用分佣系统,可以激励代理积极性,提高销售业绩,实现平台与代理的双赢。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**最后更新**: 2026-01-12
|
||||||
|
**维护人员**: Claude Sonnet 4.5
|
||||||
491
docs/iot-sim-management/实施总结.md
Normal file
491
docs/iot-sim-management/实施总结.md
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
# IoT SIM 管理系统 - 数据模型层实施总结
|
||||||
|
|
||||||
|
## 项目信息
|
||||||
|
|
||||||
|
- **项目名称**: IoT SIM 管理系统 - 数据模型层
|
||||||
|
- **实施日期**: 2026-01-12
|
||||||
|
- **实施人员**: Claude Sonnet 4.5
|
||||||
|
- **OpenSpec 变更 ID**: iot-sim-management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施范围
|
||||||
|
|
||||||
|
本次实施严格按照 OpenSpec 规范,仅完成数据模型层的实现,包括:
|
||||||
|
|
||||||
|
### ✅ 已完成
|
||||||
|
|
||||||
|
1. **数据库迁移脚本**
|
||||||
|
- UP 迁移脚本 (000005_create_iot_sim_management_tables.up.sql)
|
||||||
|
- DOWN 迁移脚本 (000005_create_iot_sim_management_tables.down.sql)
|
||||||
|
- 26 张数据库表
|
||||||
|
- 完整的索引定义
|
||||||
|
- 中文注释
|
||||||
|
- 三大运营商初始数据
|
||||||
|
|
||||||
|
2. **GORM 模型定义**
|
||||||
|
- 11 个模型文件
|
||||||
|
- 26 个模型结构体
|
||||||
|
- 遵循项目规范的字段定义
|
||||||
|
- 无外键约束,无 ORM 关联
|
||||||
|
|
||||||
|
3. **业务常量定义**
|
||||||
|
- 100+ 业务常量
|
||||||
|
- 5 大分类
|
||||||
|
- 统一常量管理
|
||||||
|
|
||||||
|
4. **代码质量保证**
|
||||||
|
- `go fmt` 格式化
|
||||||
|
- `goimports` 导入整理
|
||||||
|
- `golangci-lint` 质量检查(0 issues)
|
||||||
|
- `go build` 编译通过
|
||||||
|
|
||||||
|
5. **数据库迁移测试**
|
||||||
|
- UP 迁移成功 (616ms)
|
||||||
|
- DOWN 迁移成功 (602ms)
|
||||||
|
- 版本切换正确
|
||||||
|
|
||||||
|
6. **完整文档**
|
||||||
|
- 数据模型总结.md
|
||||||
|
- 表结构详细说明.md
|
||||||
|
- 轮询机制说明.md
|
||||||
|
- 分佣系统说明.md
|
||||||
|
- 实施总结.md (本文档)
|
||||||
|
|
||||||
|
### ❌ 不在本阶段范围
|
||||||
|
|
||||||
|
根据 OpenSpec 规范,以下工作不在本阶段实施范围:
|
||||||
|
|
||||||
|
- API Handler 层
|
||||||
|
- Service 业务逻辑层
|
||||||
|
- Store 数据访问层
|
||||||
|
- 单元测试
|
||||||
|
- 集成测试
|
||||||
|
- API 文档生成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据库表清单
|
||||||
|
|
||||||
|
### 核心业务表 (4张)
|
||||||
|
|
||||||
|
| 表名 | 说明 | 记录数 |
|
||||||
|
|------|------|--------|
|
||||||
|
| carriers | 运营商 | 3 (预置) |
|
||||||
|
| iot_cards | IoT 卡 | 0 |
|
||||||
|
| devices | 设备 | 0 |
|
||||||
|
| number_cards | 号卡 | 0 |
|
||||||
|
|
||||||
|
### 套餐与流量管理表 (7张)
|
||||||
|
|
||||||
|
| 表名 | 说明 | 记录数 |
|
||||||
|
|------|------|--------|
|
||||||
|
| package_series | 套餐系列 | 0 |
|
||||||
|
| packages | 套餐 | 0 |
|
||||||
|
| agent_package_allocations | 代理套餐分配 | 0 |
|
||||||
|
| device_sim_bindings | 设备-IoT卡绑定 | 0 |
|
||||||
|
| package_usages | 套餐使用情况 | 0 |
|
||||||
|
| polling_configs | 轮询配置 | 0 |
|
||||||
|
| data_usage_records | 流量使用记录 | 0 |
|
||||||
|
|
||||||
|
### 订单管理表 (1张)
|
||||||
|
|
||||||
|
| 表名 | 说明 | 记录数 |
|
||||||
|
|------|------|--------|
|
||||||
|
| orders | 订单 | 0 |
|
||||||
|
|
||||||
|
### 分佣系统表 (8张)
|
||||||
|
|
||||||
|
| 表名 | 说明 | 记录数 |
|
||||||
|
|------|------|--------|
|
||||||
|
| agent_hierarchies | 代理层级关系 | 0 |
|
||||||
|
| commission_rules | 分佣规则 | 0 |
|
||||||
|
| commission_ladder | 分佣阶梯 | 0 |
|
||||||
|
| commission_combined_conditions | 组合分佣条件 | 0 |
|
||||||
|
| commission_records | 分佣记录 | 0 |
|
||||||
|
| commission_approvals | 分佣审批 | 0 |
|
||||||
|
| commission_templates | 分佣模板 | 0 |
|
||||||
|
| carrier_settlements | 运营商结算 | 0 |
|
||||||
|
|
||||||
|
### 财务管理表 (3张)
|
||||||
|
|
||||||
|
| 表名 | 说明 | 记录数 |
|
||||||
|
|------|------|--------|
|
||||||
|
| commission_withdrawal_requests | 提现申请 | 0 |
|
||||||
|
| commission_withdrawal_settings | 提现设置 | 0 |
|
||||||
|
| payment_merchant_settings | 收款商户设置 | 0 |
|
||||||
|
|
||||||
|
### 系统管理表 (2张)
|
||||||
|
|
||||||
|
| 表名 | 说明 | 记录数 |
|
||||||
|
|------|------|--------|
|
||||||
|
| dev_capability_configs | 开发能力配置 | 0 |
|
||||||
|
| card_replacement_requests | 换卡申请 | 0 |
|
||||||
|
|
||||||
|
**总计**: 26 张表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
### 数据库迁移脚本
|
||||||
|
|
||||||
|
```
|
||||||
|
migrations/
|
||||||
|
├── 000005_create_iot_sim_management_tables.up.sql (1102 行)
|
||||||
|
└── 000005_create_iot_sim_management_tables.down.sql (27 行)
|
||||||
|
```
|
||||||
|
|
||||||
|
### GORM 模型文件
|
||||||
|
|
||||||
|
```
|
||||||
|
internal/iot/model/
|
||||||
|
├── carrier.go (17 行)
|
||||||
|
├── iot_card.go (40 行)
|
||||||
|
├── device.go (27 行)
|
||||||
|
├── number_card.go (26 行)
|
||||||
|
├── package.go (108 行)
|
||||||
|
├── order.go (36 行)
|
||||||
|
├── polling.go (29 行)
|
||||||
|
├── data_usage.go (20 行)
|
||||||
|
├── commission.go (175 行)
|
||||||
|
├── financial.go (70 行)
|
||||||
|
└── system.go (46 行)
|
||||||
|
```
|
||||||
|
|
||||||
|
**总计**: 11 个文件, 594 行代码
|
||||||
|
|
||||||
|
### 常量定义文件
|
||||||
|
|
||||||
|
```
|
||||||
|
pkg/constants/
|
||||||
|
└── iot.go (164 行)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文档文件
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/iot-sim-management/
|
||||||
|
├── 数据模型总结.md
|
||||||
|
├── 表结构详细说明.md
|
||||||
|
├── 轮询机制说明.md
|
||||||
|
├── 分佣系统说明.md
|
||||||
|
└── 实施总结.md (本文档)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心技术特性
|
||||||
|
|
||||||
|
### 1. 无外键约束设计
|
||||||
|
|
||||||
|
- 数据库表之间没有外键约束
|
||||||
|
- 关联关系通过存储关联 ID 字段手动维护
|
||||||
|
- 提高灵活性和性能
|
||||||
|
- 便于分布式扩展
|
||||||
|
|
||||||
|
### 2. GORM 模型规范
|
||||||
|
|
||||||
|
- 所有字段显式指定 `column:` 标签
|
||||||
|
- 禁止使用 ORM 关联关系
|
||||||
|
- 所有字段添加中文注释
|
||||||
|
- 字符串字段明确长度
|
||||||
|
- 数值字段明确精度
|
||||||
|
|
||||||
|
### 3. 多所有者模式
|
||||||
|
|
||||||
|
- `owner_type` + `owner_id` 实现多态所有权
|
||||||
|
- 支持 platform/agent/user/device 四种所有者类型
|
||||||
|
- 灵活管理资源所有权
|
||||||
|
|
||||||
|
### 4. 三层轮询机制
|
||||||
|
|
||||||
|
- 实名检查进程
|
||||||
|
- 卡流量检查进程
|
||||||
|
- 套餐流量检查进程
|
||||||
|
- 支持梯度轮询策略
|
||||||
|
- 独立的轮询配置
|
||||||
|
|
||||||
|
### 5. 三种分佣模式
|
||||||
|
|
||||||
|
- 一次性分佣 (立即发放)
|
||||||
|
- 长期分佣 (冻结后发放)
|
||||||
|
- 组合分佣 (部分立即,部分冻结)
|
||||||
|
- OR 条件解冻 (时间 OR 流量)
|
||||||
|
- 阶梯奖励机制
|
||||||
|
|
||||||
|
### 6. 真流量/虚流量共存
|
||||||
|
|
||||||
|
- 真流量额度 (`real_data_mb`)
|
||||||
|
- 虚流量额度 (`virtual_data_mb`)
|
||||||
|
- 总流量额度 (`data_amount_mb`)
|
||||||
|
- 停机判断基于虚流量
|
||||||
|
|
||||||
|
### 7. 行业卡 vs 普通卡
|
||||||
|
|
||||||
|
- 行业卡无需实名认证
|
||||||
|
- 普通卡必须实名才能激活
|
||||||
|
- 通过 `card_category` 字段区分
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码质量指标
|
||||||
|
|
||||||
|
### 编译和格式化
|
||||||
|
|
||||||
|
- ✅ `go fmt` 格式化通过
|
||||||
|
- ✅ `goimports` 导入整理通过
|
||||||
|
- ✅ `go build` 编译通过
|
||||||
|
- ✅ `go mod tidy` 依赖管理通过
|
||||||
|
|
||||||
|
### 静态分析
|
||||||
|
|
||||||
|
- ✅ `golangci-lint run` 质量检查通过
|
||||||
|
- ✅ 0 issues
|
||||||
|
- ✅ 无语法错误
|
||||||
|
- ✅ 无命名冲突
|
||||||
|
|
||||||
|
### 数据库迁移测试
|
||||||
|
|
||||||
|
- ✅ UP 迁移成功 (耗时 616ms)
|
||||||
|
- ✅ DOWN 迁移成功 (耗时 602ms)
|
||||||
|
- ✅ 版本切换正确 (4 → 5 → 4 → 5)
|
||||||
|
- ✅ 表结构验证通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 解决的问题
|
||||||
|
|
||||||
|
### 1. 常量命名冲突
|
||||||
|
|
||||||
|
**问题**: 发现订单状态常量在 `constants.go` 和 `iot.go` 中重复定义
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 将 IoT 模块的订单状态常量重命名为 `IotOrderStatus*`
|
||||||
|
- 避免与现有常量冲突
|
||||||
|
- 保持常量命名的一致性
|
||||||
|
|
||||||
|
### 2. 依赖管理
|
||||||
|
|
||||||
|
**问题**: `github.com/lib/pq` 应该作为直接依赖
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 运行 `go mod tidy` 添加直接依赖
|
||||||
|
- 确保所有依赖正确管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键决策记录
|
||||||
|
|
||||||
|
### 1. 字段命名规范
|
||||||
|
|
||||||
|
**决策**: 所有 GORM 模型字段必须显式指定 `column:` 标签
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 明确 Go 字段名和数据库字段名的映射关系
|
||||||
|
- 避免 GORM 自动转换可能带来的歧义
|
||||||
|
- 提高代码可读性和可维护性
|
||||||
|
|
||||||
|
### 2. 无外键约束
|
||||||
|
|
||||||
|
**决策**: 数据库表之间不建立外键约束
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 提高灵活性,业务逻辑完全在代码中控制
|
||||||
|
- 提高性能,无数据库层面的引用完整性检查开销
|
||||||
|
- 简化数据库 schema,迁移更容易
|
||||||
|
- 分布式友好,便于后续微服务拆分
|
||||||
|
|
||||||
|
### 3. 轮询机制设计
|
||||||
|
|
||||||
|
**决策**: 实现三层独立的轮询机制
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 实名检查、卡流量检查、套餐流量检查业务逻辑独立
|
||||||
|
- 可以针对不同场景设置不同的轮询间隔
|
||||||
|
- 提高系统可维护性和可扩展性
|
||||||
|
|
||||||
|
### 4. 分佣系统 OR 条件解冻
|
||||||
|
|
||||||
|
**决策**: 长期分佣支持时间 OR 流量两种解冻条件
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 提高解冻灵活性
|
||||||
|
- 代理满足任一条件即可获得佣金
|
||||||
|
- 提高代理积极性和用户留存率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
### 索引设计
|
||||||
|
|
||||||
|
1. **主键索引**: 所有表的 `id` 字段
|
||||||
|
2. **唯一索引**:
|
||||||
|
- `iccid` (IoT 卡唯一标识)
|
||||||
|
- `order_no` (订单号)
|
||||||
|
- `carrier_code` (运营商编码)
|
||||||
|
等唯一字段
|
||||||
|
3. **组合索引**:
|
||||||
|
- `(carrier_id, status)` - IoT 卡查询优化
|
||||||
|
- `(owner_type, owner_id)` - 所有权查询优化
|
||||||
|
- `(status, created_at)` - 订单列表查询优化
|
||||||
|
4. **单列索引**:
|
||||||
|
- 外键字段
|
||||||
|
- 常用查询字段
|
||||||
|
|
||||||
|
### 查询优化建议
|
||||||
|
|
||||||
|
1. **避免 N+1 查询**: 在业务层使用批量查询
|
||||||
|
2. **分页查询**: 列表查询必须分页,避免一次性加载大量数据
|
||||||
|
3. **异步任务**: 使用 Asynq 处理轮询任务,避免阻塞主线程
|
||||||
|
4. **缓存策略**: 运营商信息、套餐信息等静态数据可以缓存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
### 1. 数据脱敏
|
||||||
|
|
||||||
|
- 敏感信息(API 凭证、账户信息)使用 JSONB 存储
|
||||||
|
- 业务层需要实现加密/解密逻辑
|
||||||
|
|
||||||
|
### 2. 权限控制
|
||||||
|
|
||||||
|
- 数据权限通过 `owner_type` + `owner_id` 实现
|
||||||
|
- 业务层需要实现权限检查逻辑
|
||||||
|
|
||||||
|
### 3. 审计日志
|
||||||
|
|
||||||
|
- 所有表包含 `created_at` 和 `updated_at` 时间戳
|
||||||
|
- 关键操作(分佣审批、提现审批)记录审批人 ID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 后续工作建议
|
||||||
|
|
||||||
|
根据 OpenSpec 规范,数据模型层完成后,建议按以下顺序进行后续开发:
|
||||||
|
|
||||||
|
### 1. Store 数据访问层
|
||||||
|
|
||||||
|
- 实现 GORM 数据访问接口
|
||||||
|
- 实现事务管理
|
||||||
|
- 实现查询优化
|
||||||
|
|
||||||
|
### 2. Service 业务逻辑层
|
||||||
|
|
||||||
|
- 实现核心业务逻辑
|
||||||
|
- 实现轮询调度器
|
||||||
|
- 实现分佣计算引擎
|
||||||
|
- 实现运营商 Gateway 对接
|
||||||
|
|
||||||
|
### 3. Handler API 层
|
||||||
|
|
||||||
|
- 实现 RESTful API 接口
|
||||||
|
- 实现参数验证
|
||||||
|
- 实现错误处理
|
||||||
|
|
||||||
|
### 4. 测试
|
||||||
|
|
||||||
|
- 单元测试
|
||||||
|
- 集成测试
|
||||||
|
- 性能测试
|
||||||
|
|
||||||
|
### 5. 文档
|
||||||
|
|
||||||
|
- API 文档
|
||||||
|
- 部署文档
|
||||||
|
- 运维手册
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 团队协作建议
|
||||||
|
|
||||||
|
### 1. 代码审查要点
|
||||||
|
|
||||||
|
- 检查是否遵循项目规范
|
||||||
|
- 检查是否有外键约束或 ORM 关联
|
||||||
|
- 检查字段命名是否符合规范
|
||||||
|
- 检查常量是否统一管理
|
||||||
|
|
||||||
|
### 2. 数据库变更流程
|
||||||
|
|
||||||
|
- 所有数据库变更必须通过迁移脚本
|
||||||
|
- 迁移脚本必须同时编写 UP 和 DOWN
|
||||||
|
- 迁移脚本必须在测试环境验证后才能上生产
|
||||||
|
|
||||||
|
### 3. 文档维护
|
||||||
|
|
||||||
|
- 数据模型变更时同步更新文档
|
||||||
|
- 文档使用中文编写,便于团队理解
|
||||||
|
- 文档放在 `docs/` 目录统一管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 风险和挑战
|
||||||
|
|
||||||
|
### 1. 无外键约束的数据一致性
|
||||||
|
|
||||||
|
**风险**: 删除父记录时可能遗留子记录
|
||||||
|
|
||||||
|
**应对措施**:
|
||||||
|
- 在业务层实现级联删除逻辑
|
||||||
|
- 定期运行数据一致性检查脚本
|
||||||
|
- 使用软删除(status 字段)代替物理删除
|
||||||
|
|
||||||
|
### 2. 轮询任务性能
|
||||||
|
|
||||||
|
**风险**: 大量 IoT 卡可能导致轮询任务堆积
|
||||||
|
|
||||||
|
**应对措施**:
|
||||||
|
- 使用 Asynq 任务队列,支持横向扩展
|
||||||
|
- 实现限流保护,避免过度调用运营商 API
|
||||||
|
- 根据卡状态动态调整轮询间隔
|
||||||
|
|
||||||
|
### 3. 分佣计算复杂度
|
||||||
|
|
||||||
|
**风险**: 多级代理分佣计算可能影响性能
|
||||||
|
|
||||||
|
**应对措施**:
|
||||||
|
- 使用异步任务处理分佣计算
|
||||||
|
- 实现分佣计算缓存
|
||||||
|
- 定期优化分佣计算逻辑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本次实施严格按照 OpenSpec 规范完成了 IoT SIM 管理系统的数据模型层实现,包括 26 张数据库表、11 个 GORM 模型文件、100+ 业务常量,以及完整的文档。
|
||||||
|
|
||||||
|
### 成果总结
|
||||||
|
|
||||||
|
- ✅ 数据库迁移脚本 (UP + DOWN)
|
||||||
|
- ✅ GORM 模型定义 (11 个文件, 594 行代码)
|
||||||
|
- ✅ 业务常量定义 (164 行代码)
|
||||||
|
- ✅ 代码质量保证 (0 issues)
|
||||||
|
- ✅ 数据库迁移测试通过
|
||||||
|
- ✅ 完整技术文档 (5 个文档)
|
||||||
|
|
||||||
|
### 技术亮点
|
||||||
|
|
||||||
|
1. **无外键约束设计**: 提高灵活性和性能
|
||||||
|
2. **三层轮询机制**: 实名检查、卡流量检查、套餐流量检查相互独立
|
||||||
|
3. **三种分佣模式**: 一次性、长期、组合分佣,支持 OR 条件解冻
|
||||||
|
4. **多所有者模式**: 统一管理资源所有权
|
||||||
|
5. **真流量/虚流量共存**: 灵活的流量管理机制
|
||||||
|
6. **行业卡支持**: 区分行业卡和普通卡的不同业务流程
|
||||||
|
|
||||||
|
### 遵循的规范
|
||||||
|
|
||||||
|
- ✅ Go 代码风格规范 (Effective Go)
|
||||||
|
- ✅ GORM 模型规范 (显式 column 标签, 无 ORM 关联)
|
||||||
|
- ✅ 数据库设计规范 (无外键约束, 完整索引)
|
||||||
|
- ✅ 常量管理规范 (统一定义, 分类管理)
|
||||||
|
- ✅ 文档规范 (中文编写, 结构清晰)
|
||||||
|
- ✅ OpenSpec 流程规范 (只实现数据模型层)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**实施完成时间**: 2026-01-12
|
||||||
|
**实施人员**: Claude Sonnet 4.5
|
||||||
|
**文档版本**: v1.0
|
||||||
658
docs/iot-sim-management/数据模型总结.md
Normal file
658
docs/iot-sim-management/数据模型总结.md
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
# IoT SIM 管理系统 - 数据模型总结
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档总结了 IoT SIM 管理系统的数据模型层实现,包括 26 张数据库表和对应的 GORM 模型定义。
|
||||||
|
|
||||||
|
## 实现范围
|
||||||
|
|
||||||
|
- ✅ 数据库迁<E5BA93><E8BF81><EFBFBD>脚本 (migrations/000005_create_iot_sim_management_tables.up/down.sql)
|
||||||
|
- ✅ GORM 模型定义 (internal/iot/model/*.go)
|
||||||
|
- ✅ 业务常量定义 (pkg/constants/iot.go)
|
||||||
|
- ❌ API Handler 层 (不在本阶段范围)
|
||||||
|
- ❌ Service 业务逻辑层 (不在本阶段范围)
|
||||||
|
- ❌ Store 数据访问层 (不在本阶段范围)
|
||||||
|
|
||||||
|
## 核心业务实体
|
||||||
|
|
||||||
|
### 1. 运营商管理 (Carrier)
|
||||||
|
|
||||||
|
**表名**: `carriers`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `carrier_code`: 运营商编码 (CMCC/CUCC/CTCC)
|
||||||
|
- `carrier_name`: 运营商名称 (中国移动/中国联通/中国电信)
|
||||||
|
- `api_endpoint`: API 接口地址
|
||||||
|
- `api_credentials`: API 凭证 (JSONB)
|
||||||
|
|
||||||
|
**预置数据**: 初始化三大运营商数据
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/carrier.go:5`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. IoT 卡管理 (IotCard)
|
||||||
|
|
||||||
|
**表名**: `iot_cards`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `iccid`: IoT 卡唯一标识 (20 位数字)
|
||||||
|
- `card_category`: 卡业务类型 (normal-普通卡, industry-行业卡)
|
||||||
|
- `carrier_id`: 所属运营商 ID
|
||||||
|
- `owner_type`: 所有者类型 (platform/agent/user/device)
|
||||||
|
- `owner_id`: 所有者 ID
|
||||||
|
- `activation_status`: 激活状态 (0-未激活, 1-已激活)
|
||||||
|
- `real_name_status`: 实名状态 (0-未实名, 1-已实名)
|
||||||
|
- `network_status`: 网络状态 (0-停机, 1-开机)
|
||||||
|
- `enable_polling`: 是否参与轮询
|
||||||
|
|
||||||
|
**特殊机制**:
|
||||||
|
- 支持行业卡(无需实名)和普通卡(需实名)
|
||||||
|
- 多所有者模式 (owner_type + owner_id)
|
||||||
|
- 轮询开关控制 (enable_polling)
|
||||||
|
- 流量使用累计 (data_usage_mb)
|
||||||
|
- Gateway 同步时间戳 (last_sync_time)
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/iot_card.go:8`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 设备管理 (Device)
|
||||||
|
|
||||||
|
**表名**: `devices`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `device_code`: 设备唯一编码
|
||||||
|
- `device_name`: 设备名称
|
||||||
|
- `device_type`: 设备类型
|
||||||
|
- `sim_slots`: SIM 卡槽数量 (1-4)
|
||||||
|
- `owner_type`: 所有者类型 (platform/agent/user)
|
||||||
|
- `owner_id`: 所有者 ID
|
||||||
|
|
||||||
|
**关联关系**:
|
||||||
|
- 通过 `device_sim_bindings` 表关联 1-4 张 IoT 卡
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/device.go:5`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 号卡管理 (NumberCard)
|
||||||
|
|
||||||
|
**表名**: `number_cards`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `virtual_product_code`: 虚拟商品编码 (用于对应运营商订单)
|
||||||
|
- `card_name`: 号卡名称
|
||||||
|
- `carrier`: 运营商
|
||||||
|
- `data_amount_mb`: 流量额度 (MB)
|
||||||
|
- `price`: 价格 (元)
|
||||||
|
|
||||||
|
**业务说明**: 号卡是完全独立的业务线,从上游平台下单,使用虚拟商品编码映射运营商订单。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/number_card.go:8`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 套餐与流量管理
|
||||||
|
|
||||||
|
### 5. 套餐系列 (PackageSeries)
|
||||||
|
|
||||||
|
**表名**: `package_series`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `series_code`: 系列编码
|
||||||
|
- `series_name`: 系列名称
|
||||||
|
- `description`: 描述
|
||||||
|
|
||||||
|
**用途**: 套餐分组,用于一次性分佣规则配置。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/package.go:7`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 套餐 (Package)
|
||||||
|
|
||||||
|
**表名**: `packages`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `package_code`: 套餐编码
|
||||||
|
- `package_name`: 套餐名称
|
||||||
|
- `series_id`: 所属套餐系列 ID
|
||||||
|
- `package_type`: 套餐类型 (formal-正式套餐, addon-附加套餐)
|
||||||
|
- `duration_months`: 套餐时长 (月数)
|
||||||
|
- `data_type`: 流量类型 (real-真流量, virtual-虚流量)
|
||||||
|
- `real_data_mb`: 真流量额度 (MB)
|
||||||
|
- `virtual_data_mb`: 虚流量额度 (MB)
|
||||||
|
- `data_amount_mb`: 总流量额度 (MB)
|
||||||
|
- `price`: 套餐价格 (元)
|
||||||
|
|
||||||
|
**特殊机制**:
|
||||||
|
- 支持真流量/虚流量共存
|
||||||
|
- 停机判断基于虚流量
|
||||||
|
- 支持正式套餐和附加套餐
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/package.go:23`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 代理套餐分配 (AgentPackageAllocation)
|
||||||
|
|
||||||
|
**表名**: `agent_package_allocations`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `agent_id`: 代理用户 ID
|
||||||
|
- `package_id`: 套餐 ID
|
||||||
|
- `cost_price`: 成本价 (元)
|
||||||
|
- `retail_price`: 零售价 (元)
|
||||||
|
|
||||||
|
**用途**: 为直属下级代理分配套餐,设置佣金模式。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/package.go:48`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 设备-IoT 卡绑定 (DeviceSimBinding)
|
||||||
|
|
||||||
|
**表名**: `device_sim_bindings`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `device_id`: 设备 ID
|
||||||
|
- `iot_card_id`: IoT 卡 ID
|
||||||
|
- `slot_position`: 插槽位置 (1, 2, 3, 4)
|
||||||
|
- `bind_status`: 绑定状态 (1-已绑定, 2-已解绑)
|
||||||
|
- `bind_time`: 绑定时间
|
||||||
|
- `unbind_time`: 解绑时间
|
||||||
|
|
||||||
|
**用途**: 管理设备与 IoT 卡的多对多绑定关系 (1 设备绑定 1-4 张 IoT 卡)。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/package.go:66`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 套餐使用情况 (PackageUsage)
|
||||||
|
|
||||||
|
**表名**: `package_usages`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `order_id`: 订单 ID
|
||||||
|
- `package_id`: 套餐 ID
|
||||||
|
- `usage_type`: 使用类型 (single_card-单卡套餐, device-设备级套餐)
|
||||||
|
- `iot_card_id`: IoT 卡 ID (单卡套餐时有值)
|
||||||
|
- `device_id`: 设备 ID (设备级套餐时有值)
|
||||||
|
- `data_limit_mb`: 流量限额 (MB)
|
||||||
|
- `data_usage_mb`: 已使用流量 (MB)
|
||||||
|
- `real_data_usage_mb`: 真流量使用 (MB)
|
||||||
|
- `virtual_data_usage_mb`: 虚流量使用 (MB)
|
||||||
|
- `activated_at`: 套餐生效时间
|
||||||
|
- `expires_at`: 套餐过期时间
|
||||||
|
- `status`: 状态 (1-生效中, 2-已用完, 3-已过期)
|
||||||
|
- `last_package_check_at`: 最后一次套餐流量检查时间
|
||||||
|
|
||||||
|
**用途**: 跟踪单卡套餐和设备级套餐的流量使用。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/package.go:85`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. 轮询配置 (PollingConfig)
|
||||||
|
|
||||||
|
**表名**: `polling_configs`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `config_name`: 配置名称 (如 未实名卡、实名卡)
|
||||||
|
- `card_condition`: 卡状态条件 (not_real_name/real_name/activated/suspended)
|
||||||
|
- `carrier_id`: 运营商 ID (NULL 表示所有运营商)
|
||||||
|
- `real_name_check_enabled`: 是否启用实名检查
|
||||||
|
- `real_name_check_interval`: 实名检查间隔 (秒)
|
||||||
|
- `card_data_check_enabled`: 是否启用卡流量检查
|
||||||
|
- `card_data_check_interval`: 卡流量检查间隔 (秒)
|
||||||
|
- `package_check_enabled`: 是否启用套餐流量检查
|
||||||
|
- `package_check_interval`: 套餐流量检查间隔 (秒)
|
||||||
|
- `priority`: 优先级 (数字越小优先级越高)
|
||||||
|
|
||||||
|
**特殊机制**: 支持梯度轮询策略 (实名检查、卡流量检查、套餐流量检查)。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/polling.go:7`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. 流量使用记录 (DataUsageRecord)
|
||||||
|
|
||||||
|
**表名**: `data_usage_records`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `iot_card_id`: IoT 卡 ID
|
||||||
|
- `usage_date`: 使用日期
|
||||||
|
- `data_usage_mb`: 流量使用 (MB)
|
||||||
|
- `carrier_sync_data`: 运营商同步数据 (JSONB)
|
||||||
|
- `synced_at`: 同步时间
|
||||||
|
|
||||||
|
**用途**: 记录 IoT 卡每日流量使用情况 (历史数据)。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/data_usage.go:5`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 订单管理
|
||||||
|
|
||||||
|
### 12. 订单 (Order)
|
||||||
|
|
||||||
|
**表名**: `orders`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `order_no`: 订单号 (唯一标识)
|
||||||
|
- `order_type`: 订单类型 (1-套餐订单, 2-号卡订单)
|
||||||
|
- `iot_card_id`: IoT 卡 ID (单卡套餐订单时有值)
|
||||||
|
- `device_id`: 设备 ID (设备级套餐订单时有值)
|
||||||
|
- `number_card_id`: 号卡 ID (号卡订单时有值)
|
||||||
|
- `package_id`: 套餐 ID (套餐订单时有值)
|
||||||
|
- `user_id`: 用户 ID
|
||||||
|
- `agent_id`: 代理用户 ID
|
||||||
|
- `amount`: 订单金额 (元)
|
||||||
|
- `payment_method`: 支付方式 (wallet/online/carrier)
|
||||||
|
- `status`: 状态 (1-待支付, 2-已支付, 3-已完成, 4-已取消, 5-已退款)
|
||||||
|
- `carrier_order_id`: 运营商订单 ID
|
||||||
|
- `carrier_order_data`: 运营商订单原始数据 (JSONB)
|
||||||
|
|
||||||
|
**支持场景**:
|
||||||
|
- 套餐订单 (单卡套餐 / 设备级套餐)
|
||||||
|
- 号卡订单
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/order.go:11`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分佣系统
|
||||||
|
|
||||||
|
### 13. 代理层级关系 (AgentHierarchy)
|
||||||
|
|
||||||
|
**表名**: `agent_hierarchies`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `agent_id`: 代理用户 ID
|
||||||
|
- `parent_agent_id`: 上级代理用户 ID
|
||||||
|
- `agent_level`: 代理层级 (1-一级代理, 2-二级代理, ...)
|
||||||
|
- `agent_path`: 代理路径 (如 /1/2/3/)
|
||||||
|
|
||||||
|
**用途**: 管理代理的树形层级关系。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/commission.go:8`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. 分佣规则 (CommissionRule)
|
||||||
|
|
||||||
|
**表名**: `commission_rules`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `rule_name`: 规则名称
|
||||||
|
- `rule_type`: 规则类型 (one_time-一次性分佣, long_term-长期分佣, combined-组合分佣)
|
||||||
|
- `package_series_id`: 套餐系列 ID
|
||||||
|
- `commission_type`: 分佣方式 (fixed-固定金额, percentage-百分比)
|
||||||
|
- `commission_value`: 分佣值
|
||||||
|
- `target_level`: 目标层级 (NULL 表示所有层级)
|
||||||
|
- `enable_ladder`: 是否启用阶梯
|
||||||
|
- `freeze_days`: 冻结天数 (长期分佣)
|
||||||
|
- `freeze_data_mb`: 冻结流量 (MB, 长期分佣)
|
||||||
|
- `unfreeze_mode`: 解冻模式 (auto-自动, manual-手动)
|
||||||
|
|
||||||
|
**分佣类型说明**:
|
||||||
|
- **一次性分佣**: 订单完成后立即发放
|
||||||
|
- **长期分佣**: 订单完成后冻结,满足解冻条件后发放
|
||||||
|
- **组合分佣**: 同时包含一次性和长期分佣
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/commission.go:24`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. 分佣阶梯 (CommissionLadder)
|
||||||
|
|
||||||
|
**表名**: `commission_ladder`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `rule_id`: 分佣规则 ID
|
||||||
|
- `min_quantity`: 最小数量
|
||||||
|
- `max_quantity`: 最大数量
|
||||||
|
- `commission_type`: 分佣方式 (fixed/percentage)
|
||||||
|
- `commission_value`: 分佣值
|
||||||
|
|
||||||
|
**用途**: 为分佣规则配置阶梯奖励 (订单数量越多,分佣越高)。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/commission.go:47`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. 组合分佣条件 (CommissionCombinedCondition)
|
||||||
|
|
||||||
|
**表名**: `commission_combined_conditions`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `rule_id`: 分佣规则 ID
|
||||||
|
- `condition_type`: 条件类型 (one_time-一次性, long_term-长期)
|
||||||
|
- `commission_type`: 分佣方式 (fixed/percentage)
|
||||||
|
- `commission_value`: 分佣值
|
||||||
|
- `freeze_days`: 冻结天数 (长期分佣)
|
||||||
|
- `freeze_data_mb`: 冻结流量 (MB, 长期分佣)
|
||||||
|
|
||||||
|
**用途**: 定义组合分佣规则的具体条件 (一次性部分 + 长期部分)。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/commission.go:68`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 17. 分佣记录 (CommissionRecord)
|
||||||
|
|
||||||
|
**表名**: `commission_records`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `order_id`: 订单 ID
|
||||||
|
- `agent_id`: 代理用户 ID
|
||||||
|
- `rule_id`: 分佣规则 ID
|
||||||
|
- `commission_type`: 分佣类型 (one_time/long_term/combined)
|
||||||
|
- `commission_amount`: 分佣金额 (元)
|
||||||
|
- `status`: 状态 (1-待发放, 2-已发放, 3-已冻结, 4-已取消)
|
||||||
|
- `freeze_days`: 冻结天数
|
||||||
|
- `freeze_data_mb`: 冻结流量 (MB)
|
||||||
|
- `unfreeze_conditions`: 解冻条件 (JSONB)
|
||||||
|
- `unfrozen_at`: 解冻时间
|
||||||
|
- `distributed_at`: 发放时间
|
||||||
|
|
||||||
|
**特殊机制**: 支持 OR 条件解冻 (时间到期 OR 流量达标,满足其一即可解冻)。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/commission.go:88`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. 分佣审批 (CommissionApproval)
|
||||||
|
|
||||||
|
**表名**: `commission_approvals`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `commission_record_id`: 分佣记录 ID
|
||||||
|
- `agent_id`: 代理用户 ID
|
||||||
|
- `approval_status`: 审批状态 (1-待审批, 2-已通过, 3-已拒绝)
|
||||||
|
- `approver_id`: 审批人 ID
|
||||||
|
- `approval_reason`: 审批原因
|
||||||
|
- `approved_at`: 审批时间
|
||||||
|
|
||||||
|
**用途**: 管理需要手动审批的分佣记录。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/commission.go:116`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 19. 分佣模板 (CommissionTemplate)
|
||||||
|
|
||||||
|
**表名**: `commission_templates`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `template_name`: 模板名称
|
||||||
|
- `template_data`: 模板数据 (JSONB)
|
||||||
|
- `description`: 描述
|
||||||
|
|
||||||
|
**用途**: 快速创建分佣规则的预设模板。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/commission.go:137`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 20. 运营商结算 (CarrierSettlement)
|
||||||
|
|
||||||
|
**表名**: `carrier_settlements`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `carrier_id`: 运营商 ID
|
||||||
|
- `settlement_month`: 结算月份 (YYYY-MM)
|
||||||
|
- `total_orders`: 总订单数
|
||||||
|
- `total_amount`: 总金额 (元)
|
||||||
|
- `settlement_status`: 结算状态 (1-待结算, 2-已结算, 3-已支付)
|
||||||
|
- `settled_at`: 结算时间
|
||||||
|
- `paid_at`: 支付时间
|
||||||
|
|
||||||
|
**用途**: 记录与运营商的月度结算情况。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/commission.go:155`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 财务管理
|
||||||
|
|
||||||
|
### 21. 提现申请 (CommissionWithdrawalRequest)
|
||||||
|
|
||||||
|
**表名**: `commission_withdrawal_requests`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `agent_id`: 代理用户 ID
|
||||||
|
- `withdrawal_amount`: 提现金额 (元)
|
||||||
|
- `withdrawal_method`: 提现方式 (bank_card/alipay/wechat)
|
||||||
|
- `account_info`: 账户信息 (JSONB)
|
||||||
|
- `status`: 状态 (1-待审核, 2-已通过, 3-已拒绝, 4-已打款, 5-已取消)
|
||||||
|
- `reviewer_id`: 审核人 ID
|
||||||
|
- `review_reason`: 审核原因
|
||||||
|
- `reviewed_at`: 审核时间
|
||||||
|
- `paid_at`: 打款时间
|
||||||
|
|
||||||
|
**用途**: 管理代理用户的佣金提现申请。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/financial.go:8`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 22. 提现设置 (CommissionWithdrawalSetting)
|
||||||
|
|
||||||
|
**表名**: `commission_withdrawal_settings`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `agent_id`: 代理用户 ID
|
||||||
|
- `min_withdrawal_amount`: 最小提现金额 (元)
|
||||||
|
- `max_withdrawal_amount`: 最大提现金额 (元)
|
||||||
|
- `withdrawal_fee_rate`: 提现手续费率 (小数)
|
||||||
|
- `auto_approval_enabled`: 是否启用自动审批
|
||||||
|
|
||||||
|
**用途**: 配置代理用户的提现规则。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/financial.go:31`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 23. 收款商户设置 (PaymentMerchantSetting)
|
||||||
|
|
||||||
|
**表名**: `payment_merchant_settings`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `merchant_name`: 商户名称
|
||||||
|
- `merchant_type`: 商户类型 (alipay/wechat/bank)
|
||||||
|
- `merchant_config`: 商户配置 (JSONB)
|
||||||
|
- `is_default`: 是否默认商户
|
||||||
|
|
||||||
|
**用途**: 配置收款商户信息 (支付宝、微信、银行)。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/financial.go:51`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统管理
|
||||||
|
|
||||||
|
### 24. 开发能力配置 (DevCapabilityConfig)
|
||||||
|
|
||||||
|
**表名**: `dev_capability_configs`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `capability_name`: 能力名称
|
||||||
|
- `capability_code`: 能力编码
|
||||||
|
- `capability_config`: 能力配置 (JSONB)
|
||||||
|
- `description`: 描述
|
||||||
|
|
||||||
|
**用途**: 管理系统开发能力配置 (如 API 开关、功能权限等)。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/system.go:5`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 25. 换卡申请 (CardReplacementRequest)
|
||||||
|
|
||||||
|
**表名**: `card_replacement_requests`
|
||||||
|
|
||||||
|
**核心字段**:
|
||||||
|
- `old_iot_card_id`: 旧卡 ID
|
||||||
|
- `new_iot_card_id`: 新卡 ID
|
||||||
|
- `user_id`: 用户 ID
|
||||||
|
- `replacement_reason`: 换卡原因
|
||||||
|
- `status`: 状态 (1-待审核, 2-已通过, 3-已拒绝, 4-已完成)
|
||||||
|
- `reviewer_id`: 审核人 ID
|
||||||
|
- `reviewed_at`: 审核时间
|
||||||
|
- `completed_at`: 完成时间
|
||||||
|
|
||||||
|
**用途**: 管理 IoT 卡的换卡申请流程。
|
||||||
|
|
||||||
|
**文件位置**: `internal/iot/model/system.go:25`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据库设计原则
|
||||||
|
|
||||||
|
### 1. 无外键约束
|
||||||
|
|
||||||
|
- 数据库表之间**禁止建立外键约束** (Foreign Key Constraints)
|
||||||
|
- 关联关系通过存储关联 ID 字段手动维护
|
||||||
|
- 关联数据查询在代码层面显式执行
|
||||||
|
|
||||||
|
**设计理由**:
|
||||||
|
- 灵活性:业务逻辑完全在代码中控制
|
||||||
|
- 性能:无数据库层面的引用完整性检查开销
|
||||||
|
- 可控性:开发者完全掌控何时查询关联数据
|
||||||
|
- 分布式友好:在微服务场景下更容易扩展
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. GORM 模型规范
|
||||||
|
|
||||||
|
- 所有字段必须显式指定 `column:` 标签
|
||||||
|
- 禁止使用 ORM 关联关系 (`foreignKey`, `references`, `hasMany`, `belongsTo`)
|
||||||
|
- 所有字段必须添加中文注释
|
||||||
|
- 字符串字段长度必须明确定义 (VARCHAR(100)/VARCHAR(255)/TEXT)
|
||||||
|
- 数值字段精度必须明确定义 (DECIMAL(10,2)/BIGINT)
|
||||||
|
- 时间字段使用 GORM 自动管理 (`autoCreateTime`, `autoUpdateTime`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 命名规范
|
||||||
|
|
||||||
|
- 数据库字段名:下划线命名法 (snake_case),如 `user_id`, `created_at`
|
||||||
|
- Go 结构体字段名:驼峰命名法 (PascalCase),如 `UserID`, `CreatedAt`
|
||||||
|
- 表名:复数形式,如 `iot_cards`, `orders`, `commission_rules`
|
||||||
|
- 常量名:大写驼峰 + 前缀,如 `IotOrderStatusPending`, `CarrierCodeCMCC`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常量定义
|
||||||
|
|
||||||
|
所有业务常量统一定义在 `pkg/constants/iot.go` 文件中,包括:
|
||||||
|
|
||||||
|
- 卡类型、卡业务类型、激活状态、实名状态、网络状态
|
||||||
|
- 所有者类型、卡状态、设备状态
|
||||||
|
- 套餐类型、流量类型、套餐使用状态
|
||||||
|
- 订单类型、订单状态、支付方式
|
||||||
|
- 轮询卡状态条件
|
||||||
|
- 分佣规则类型、分佣方式、分佣状态、解冻模式
|
||||||
|
- 提现状态、提现方式、商户类型
|
||||||
|
- 换卡申请状态、审批状态
|
||||||
|
|
||||||
|
**文件位置**: `pkg/constants/iot.go:1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据库迁移
|
||||||
|
|
||||||
|
### 迁移脚本
|
||||||
|
|
||||||
|
- **UP 脚本**: `migrations/000005_create_iot_sim_management_tables.up.sql`
|
||||||
|
- 创建 26 张表
|
||||||
|
- 创建所有必需的索引
|
||||||
|
- 添加完整的中文注释
|
||||||
|
- 初始化三大运营商数据
|
||||||
|
|
||||||
|
- **DOWN 脚本**: `migrations/000005_create_iot_sim_management_tables.down.sql`
|
||||||
|
- 按反向依赖顺序删除所有表
|
||||||
|
|
||||||
|
### 迁移测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 应用迁移
|
||||||
|
source .env && migrate -database "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" -path migrations up
|
||||||
|
|
||||||
|
# 回滚迁移
|
||||||
|
source .env && migrate -database "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" -path migrations down 1
|
||||||
|
|
||||||
|
# 查看版本
|
||||||
|
source .env && migrate -database "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" -path migrations version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试结果
|
||||||
|
|
||||||
|
- ✅ UP 迁移成功 (耗时 616ms)
|
||||||
|
- ✅ DOWN 迁移成功 (耗时 602ms)
|
||||||
|
- ✅ 数据库版本正确切换 (4 → 5 → 4 → 5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
### 数据库迁移脚本
|
||||||
|
- `migrations/000005_create_iot_sim_management_tables.up.sql` (1102 行)
|
||||||
|
- `migrations/000005_create_iot_sim_management_tables.down.sql` (27 行)
|
||||||
|
|
||||||
|
### GORM 模型定义
|
||||||
|
- `internal/iot/model/carrier.go` (17 行)
|
||||||
|
- `internal/iot/model/iot_card.go` (40 行)
|
||||||
|
- `internal/iot/model/device.go` (27 行)
|
||||||
|
- `internal/iot/model/number_card.go` (26 行)
|
||||||
|
- `internal/iot/model/package.go` (108 行)
|
||||||
|
- `internal/iot/model/order.go` (36 行)
|
||||||
|
- `internal/iot/model/polling.go` (29 行)
|
||||||
|
- `internal/iot/model/data_usage.go` (20 行)
|
||||||
|
- `internal/iot/model/commission.go` (175 行)
|
||||||
|
- `internal/iot/model/financial.go` (70 行)
|
||||||
|
- `internal/iot/model/system.go` (46 行)
|
||||||
|
|
||||||
|
### 常量定义
|
||||||
|
- `pkg/constants/iot.go` (164 行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码质量
|
||||||
|
|
||||||
|
- ✅ `go fmt` 格式化通过
|
||||||
|
- ✅ `goimports` 导入整理通过
|
||||||
|
- ✅ `golangci-lint` 质量检查通过 (0 issues)
|
||||||
|
- ✅ `go build` 编译通过
|
||||||
|
- ✅ `go mod tidy` 依赖管理通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步工作
|
||||||
|
|
||||||
|
根据 OpenSpec 规范,本阶段只实现数据模型层,以下工作不在本阶段范围:
|
||||||
|
|
||||||
|
- ❌ API Handler 层
|
||||||
|
- ❌ Service 业务逻辑层
|
||||||
|
- ❌ Store 数据访问层
|
||||||
|
- ❌ 单元测试
|
||||||
|
- ❌ 集成测试
|
||||||
|
- ❌ API 文档生成
|
||||||
|
|
||||||
|
这些工作将在后续阶段按照 OpenSpec 流程逐步实现。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考文档
|
||||||
|
|
||||||
|
- [表结构详细说明](./表结构详细说明.md)
|
||||||
|
- [轮询机制说明](./轮询机制说明.md)
|
||||||
|
- [分佣系统说明](./分佣系统说明.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**最后更新**: 2026-01-12
|
||||||
|
**维护人员**: Claude Sonnet 4.5
|
||||||
1020
docs/iot-sim-management/表结构详细说明.md
Normal file
1020
docs/iot-sim-management/表结构详细说明.md
Normal file
File diff suppressed because it is too large
Load Diff
776
docs/iot-sim-management/轮询机制说明.md
Normal file
776
docs/iot-sim-management/轮询机制说明.md
Normal file
@@ -0,0 +1,776 @@
|
|||||||
|
# IoT SIM 管理系统 - 轮询机制说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
IoT SIM 管理系统实现了一套灵活的三层轮询机制,用于定期检查 IoT 卡的实名状态、流量使用情况和套餐流量情况。轮询机制支持梯度策略配置,可以针对不同卡状态、不同运营商设置不同的轮询间隔和优先级。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 轮询架构
|
||||||
|
|
||||||
|
### 三层轮询体系
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ 轮询调度器 │
|
||||||
|
│ (Polling Scheduler) │
|
||||||
|
│ - 读取轮询配置表 │
|
||||||
|
│ - 按优先级和间隔时间调度任务 │
|
||||||
|
│ - 使用 Asynq 异步任务队列 │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────┼──────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||||
|
│ 实名检查进程 │ │ 卡流量检查进程 │ │ 套餐流量检查进程 │
|
||||||
|
│ (Real Name) │ │ (Card Data) │ │ (Package Data) │
|
||||||
|
├────────────────┤ ├────────────────┤ ├────────────────┤
|
||||||
|
│ - 查询未实名卡 │ │ - 查询激活的卡 │ │ - 查询生效中套餐 │
|
||||||
|
│ - 调用运营商API │ │ - 同步流量使用 │ │ - 检查流量使用 │
|
||||||
|
│ - 更新实名状态 │ │ - 更新 IoT 卡 │ │ - 判断是否停机 │
|
||||||
|
└────────────────┘ └────────────────┘ └────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 轮询配置表
|
||||||
|
|
||||||
|
### 表结构: `polling_configs`
|
||||||
|
|
||||||
|
轮询配置表支持灵活的梯度轮询策略:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE polling_configs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
config_name VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
description VARCHAR(500),
|
||||||
|
card_condition VARCHAR(50),
|
||||||
|
carrier_id BIGINT,
|
||||||
|
real_name_check_enabled BOOLEAN DEFAULT false,
|
||||||
|
real_name_check_interval INT DEFAULT 60,
|
||||||
|
card_data_check_enabled BOOLEAN DEFAULT false,
|
||||||
|
card_data_check_interval INT DEFAULT 60,
|
||||||
|
package_check_enabled BOOLEAN DEFAULT false,
|
||||||
|
package_check_interval INT DEFAULT 60,
|
||||||
|
priority INT NOT NULL DEFAULT 100,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置字段说明
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| config_name | VARCHAR(100) | 配置名称,如"未实名卡-移动"、"已激活卡-联通" |
|
||||||
|
| card_condition | VARCHAR(50) | 卡状态条件: `not_real_name`/`real_name`/`activated`/`suspended` |
|
||||||
|
| carrier_id | BIGINT | 运营商ID,NULL 表示所有运营商 |
|
||||||
|
| real_name_check_enabled | BOOLEAN | 是否启用实名检查 |
|
||||||
|
| real_name_check_interval | INT | 实名检查间隔(秒) |
|
||||||
|
| card_data_check_enabled | BOOLEAN | 是否启用卡流量检查 |
|
||||||
|
| card_data_check_interval | INT | 卡流量检查间隔(秒) |
|
||||||
|
| package_check_enabled | BOOLEAN | 是否启用套餐流量检查 |
|
||||||
|
| package_check_interval | INT | 套餐流量检查间隔(秒) |
|
||||||
|
| priority | INT | 优先级(数字越小优先级越高) |
|
||||||
|
| status | INT | 状态: 1-启用, 2-禁用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 轮询配置示例
|
||||||
|
|
||||||
|
### 示例 1: 未实名卡快速轮询
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO polling_configs (
|
||||||
|
config_name,
|
||||||
|
description,
|
||||||
|
card_condition,
|
||||||
|
carrier_id,
|
||||||
|
real_name_check_enabled,
|
||||||
|
real_name_check_interval,
|
||||||
|
card_data_check_enabled,
|
||||||
|
card_data_check_interval,
|
||||||
|
priority,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
'未实名卡-快速轮询',
|
||||||
|
'对未实名卡每30秒检查一次实名状态',
|
||||||
|
'not_real_name',
|
||||||
|
NULL, -- 所有运营商
|
||||||
|
true, -- 启用实名检查
|
||||||
|
30, -- 30秒间隔
|
||||||
|
false, -- 不检查流量
|
||||||
|
0,
|
||||||
|
10, -- 高优先级
|
||||||
|
1 -- 启用
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 未实名卡需要频繁检查实名状态,以便及时发现已完成实名认证的卡。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 2: 已激活卡流量监控
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO polling_configs (
|
||||||
|
config_name,
|
||||||
|
description,
|
||||||
|
card_condition,
|
||||||
|
carrier_id,
|
||||||
|
real_name_check_enabled,
|
||||||
|
real_name_check_interval,
|
||||||
|
card_data_check_enabled,
|
||||||
|
card_data_check_interval,
|
||||||
|
package_check_enabled,
|
||||||
|
package_check_interval,
|
||||||
|
priority,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
'已激活卡-流量监控',
|
||||||
|
'对已激活卡每60秒检查流量使用',
|
||||||
|
'activated',
|
||||||
|
NULL,
|
||||||
|
false, -- 不检查实名
|
||||||
|
0,
|
||||||
|
true, -- 启用卡流量检查
|
||||||
|
60, -- 60秒间隔
|
||||||
|
true, -- 启用套餐流量检查
|
||||||
|
60, -- 60秒间隔
|
||||||
|
20, -- 中优先级
|
||||||
|
1
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 已激活卡需要监控流量使用,防止超额使用和及时停机。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 3: 移动运营商特殊策略
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO polling_configs (
|
||||||
|
config_name,
|
||||||
|
description,
|
||||||
|
card_condition,
|
||||||
|
carrier_id,
|
||||||
|
real_name_check_enabled,
|
||||||
|
real_name_check_interval,
|
||||||
|
card_data_check_enabled,
|
||||||
|
card_data_check_interval,
|
||||||
|
priority,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
'移动-已激活卡-慢速轮询',
|
||||||
|
'移动运营商已激活卡每180秒检查一次流量',
|
||||||
|
'activated',
|
||||||
|
1, -- 中国移动 carrier_id
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
180, -- 180秒间隔
|
||||||
|
50, -- 低优先级
|
||||||
|
1
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 可以针对特定运营商设置不同的轮询策略,优化 API 调用频率。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三种轮询进程
|
||||||
|
|
||||||
|
### 1. 实名检查进程 (Real Name Check)
|
||||||
|
|
||||||
|
**目标**: 检查未实名的 IoT 卡是否已完成实名认证
|
||||||
|
|
||||||
|
**工作流程**:
|
||||||
|
1. 查询符合条件的 IoT 卡:
|
||||||
|
- `card_category = 'normal'` (普通卡需要实名)
|
||||||
|
- `real_name_status = 0` (未实名)
|
||||||
|
- `enable_polling = true` (参与轮询)
|
||||||
|
- 根据 `last_real_name_check_at` 判断是否到达检查间隔
|
||||||
|
2. 调用运营商 Gateway API 查询实名状态
|
||||||
|
3. 更新 IoT 卡的 `real_name_status` 和 `last_real_name_check_at`
|
||||||
|
4. 记录日志和异常情况
|
||||||
|
|
||||||
|
**轮询间隔控制**:
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
if time.Since(card.LastRealNameCheckAt) >= config.RealNameCheckInterval {
|
||||||
|
// 执行实名检查
|
||||||
|
result := gateway.CheckRealName(card.ICCID)
|
||||||
|
card.RealNameStatus = result.Status
|
||||||
|
card.LastRealNameCheckAt = time.Now()
|
||||||
|
db.Save(&card)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置参数**:
|
||||||
|
- `real_name_check_enabled`: 是否启用
|
||||||
|
- `real_name_check_interval`: 检查间隔(秒)
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 行业卡 (`card_category = 'industry'`) 无需实名检查
|
||||||
|
- 已实名的卡 (`real_name_status = 1`) 不再参与轮询
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 卡流量检查进程 (Card Data Check)
|
||||||
|
|
||||||
|
**目标**: 同步 IoT 卡的流量使用情况
|
||||||
|
|
||||||
|
**工作流程**:
|
||||||
|
1. 查询符合条件的 IoT 卡:
|
||||||
|
- `activation_status = 1` (已激活)
|
||||||
|
- `enable_polling = true` (参与轮询)
|
||||||
|
- 根据 `last_data_check_at` 判断是否到达检查间隔
|
||||||
|
2. 调用运营商 Gateway API 查询流量使用
|
||||||
|
3. 更新 IoT 卡的 `data_usage_mb` 和 `last_data_check_at`
|
||||||
|
4. 记录流量使用历史到 `data_usage_records` 表
|
||||||
|
5. 判断是否需要停机:
|
||||||
|
- 如果流量超过套餐的虚流量额度,触发停机逻辑
|
||||||
|
|
||||||
|
**轮询间隔控制**:
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
if time.Since(card.LastDataCheckAt) >= config.CardDataCheckInterval {
|
||||||
|
// 执行流量检查
|
||||||
|
usage := gateway.GetDataUsage(card.ICCID)
|
||||||
|
card.DataUsageMB = usage.TotalMB
|
||||||
|
card.LastDataCheckAt = time.Now()
|
||||||
|
db.Save(&card)
|
||||||
|
|
||||||
|
// 记录历史数据
|
||||||
|
record := DataUsageRecord{
|
||||||
|
IotCardID: card.ID,
|
||||||
|
UsageDate: time.Now().Format("2006-01-02"),
|
||||||
|
DataUsageMB: usage.TodayMB,
|
||||||
|
CarrierSyncData: usage.RawData,
|
||||||
|
SyncedAt: time.Now(),
|
||||||
|
}
|
||||||
|
db.Create(&record)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置参数**:
|
||||||
|
- `card_data_check_enabled`: 是否启用
|
||||||
|
- `card_data_check_interval`: 检查间隔(秒)
|
||||||
|
|
||||||
|
**停机判断逻辑**:
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
if card.DataUsageMB >= package.VirtualDataMB {
|
||||||
|
// 触发停机
|
||||||
|
card.NetworkStatus = 0 // 停机
|
||||||
|
gateway.SuspendCard(card.ICCID)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 套餐流量检查进程 (Package Check)
|
||||||
|
|
||||||
|
**目标**: 检查套餐流量使用情况,判断套餐状态
|
||||||
|
|
||||||
|
**工作流程**:
|
||||||
|
1. 查询符合条件的套餐使用记录:
|
||||||
|
- `status = 1` (生效中)
|
||||||
|
- `expires_at > NOW()` (未过期)
|
||||||
|
- 根据 `last_package_check_at` 判断是否到达检查间隔
|
||||||
|
2. 计算套餐的流量使用情况:
|
||||||
|
- 单卡套餐: 统计该卡的流量使用
|
||||||
|
- 设备级套餐: 统计该设备绑定的所有卡的流量使用
|
||||||
|
3. 更新套餐使用记录的 `data_usage_mb` 和 `last_package_check_at`
|
||||||
|
4. 判断套餐状态:
|
||||||
|
- 如果流量用完: `status = 2` (已用完)
|
||||||
|
- 如果时间过期: `status = 3` (已过期)
|
||||||
|
5. 如果套餐用完或过期,触发停机逻辑
|
||||||
|
|
||||||
|
**轮询间隔控制**:
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
if time.Since(packageUsage.LastPackageCheckAt) >= config.PackageCheckInterval {
|
||||||
|
// 执行套餐检查
|
||||||
|
var totalUsage int64
|
||||||
|
|
||||||
|
if packageUsage.UsageType == "single_card" {
|
||||||
|
// 单卡套餐
|
||||||
|
card := db.FindIotCardByID(packageUsage.IotCardID)
|
||||||
|
totalUsage = card.DataUsageMB
|
||||||
|
} else {
|
||||||
|
// 设备级套餐
|
||||||
|
bindings := db.FindDeviceSimBindings(packageUsage.DeviceID)
|
||||||
|
for _, binding := range bindings {
|
||||||
|
card := db.FindIotCardByID(binding.IotCardID)
|
||||||
|
totalUsage += card.DataUsageMB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packageUsage.DataUsageMB = totalUsage
|
||||||
|
packageUsage.LastPackageCheckAt = time.Now()
|
||||||
|
|
||||||
|
// 判断状态
|
||||||
|
if totalUsage >= packageUsage.DataLimitMB {
|
||||||
|
packageUsage.Status = 2 // 已用完
|
||||||
|
// 触发停机
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(packageUsage.ExpiresAt) {
|
||||||
|
packageUsage.Status = 3 // 已过期
|
||||||
|
// 触发停机
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Save(&packageUsage)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置参数**:
|
||||||
|
- `package_check_enabled`: 是否启用
|
||||||
|
- `package_check_interval`: 检查间隔(秒)
|
||||||
|
|
||||||
|
**停机判断逻辑**:
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
if packageUsage.Status == 2 || packageUsage.Status == 3 {
|
||||||
|
// 套餐用完或过期,触发停机
|
||||||
|
if packageUsage.UsageType == "single_card" {
|
||||||
|
card := db.FindIotCardByID(packageUsage.IotCardID)
|
||||||
|
card.NetworkStatus = 0 // 停机
|
||||||
|
gateway.SuspendCard(card.ICCID)
|
||||||
|
} else {
|
||||||
|
// 设备级套餐,停掉所有绑定的卡
|
||||||
|
bindings := db.FindDeviceSimBindings(packageUsage.DeviceID)
|
||||||
|
for _, binding := range bindings {
|
||||||
|
card := db.FindIotCardByID(binding.IotCardID)
|
||||||
|
card.NetworkStatus = 0
|
||||||
|
gateway.SuspendCard(card.ICCID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 轮询调度器设计
|
||||||
|
|
||||||
|
### 调度器架构
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
type PollingScheduler struct {
|
||||||
|
db *gorm.DB
|
||||||
|
queue *asynq.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PollingScheduler) Start() {
|
||||||
|
// 启动三个独立的调度协程
|
||||||
|
go s.scheduleRealNameCheck()
|
||||||
|
go s.scheduleCardDataCheck()
|
||||||
|
go s.schedulePackageCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PollingScheduler) scheduleRealNameCheck() {
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
configs := s.loadPollingConfigs("real_name_check_enabled = true")
|
||||||
|
|
||||||
|
for _, config := range configs {
|
||||||
|
// 查询需要检查的卡
|
||||||
|
cards := s.findCardsForRealNameCheck(config)
|
||||||
|
|
||||||
|
for _, card := range cards {
|
||||||
|
// 使用 Asynq 异步任务队列
|
||||||
|
task := asynq.NewTask("iot:realname:check", map[string]interface{}{
|
||||||
|
"card_id": card.ID,
|
||||||
|
"config_id": config.ID,
|
||||||
|
})
|
||||||
|
s.queue.Enqueue(task, asynq.ProcessIn(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 任务队列设计
|
||||||
|
|
||||||
|
使用 Asynq 异步任务队列处理轮询任务:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
type RealNameCheckHandler struct {
|
||||||
|
db *gorm.DB
|
||||||
|
gateway *CarrierGateway
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RealNameCheckHandler) ProcessTask(ctx context.Context, task *asynq.Task) error {
|
||||||
|
var payload struct {
|
||||||
|
CardID uint `json:"card_id"`
|
||||||
|
ConfigID uint `json:"config_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(task.Payload(), &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载卡信息
|
||||||
|
card := h.db.FindIotCardByID(payload.CardID)
|
||||||
|
|
||||||
|
// 调用运营商 API
|
||||||
|
result, err := h.gateway.CheckRealName(card.ICCID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新卡状态
|
||||||
|
card.RealNameStatus = result.Status
|
||||||
|
card.LastRealNameCheckAt = time.Now()
|
||||||
|
h.db.Save(&card)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 轮询优先级和并发控制
|
||||||
|
|
||||||
|
### 优先级机制
|
||||||
|
|
||||||
|
轮询配置表的 `priority` 字段控制执行优先级:
|
||||||
|
|
||||||
|
- **高优先级 (1-30)**: 紧急任务,如未实名卡检查
|
||||||
|
- **中优先级 (31-70)**: 常规任务,如流量监控
|
||||||
|
- **低优先级 (71-100)**: 非紧急任务,如历史数据同步
|
||||||
|
|
||||||
|
调度器按优先级排序执行:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM polling_configs
|
||||||
|
WHERE status = 1
|
||||||
|
ORDER BY priority ASC, id ASC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 并发控制
|
||||||
|
|
||||||
|
使用 Asynq 的并发控制功能:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
queue := asynq.NewClient(asynq.RedisClientOpt{Addr: "localhost:6379"})
|
||||||
|
|
||||||
|
// 设置队列并发数
|
||||||
|
queues := map[string]int{
|
||||||
|
"iot:realname": 10, // 实名检查队列,10个并发
|
||||||
|
"iot:carddata": 20, // 卡流量检查队列,20个并发
|
||||||
|
"iot:package": 20, // 套餐检查队列,20个并发
|
||||||
|
}
|
||||||
|
|
||||||
|
server := asynq.NewServer(
|
||||||
|
asynq.RedisClientOpt{Addr: "localhost:6379"},
|
||||||
|
asynq.Config{Queues: queues},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 限流保护
|
||||||
|
|
||||||
|
为了避免过度调用运营商 API,需要实现限流保护:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
type RateLimiter struct {
|
||||||
|
limiter *rate.Limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRateLimiter(carrierID uint) *RateLimiter {
|
||||||
|
// 每秒最多 10 次 API 调用
|
||||||
|
return &RateLimiter{
|
||||||
|
limiter: rate.NewLimiter(rate.Limit(10), 10),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RateLimiter) Wait(ctx context.Context) error {
|
||||||
|
return r.limiter.Wait(ctx)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 轮询间隔策略
|
||||||
|
|
||||||
|
### 推荐配置
|
||||||
|
|
||||||
|
| 卡状态 | 实名检查间隔 | 卡流量检查间隔 | 套餐流量检查间隔 | 优先级 |
|
||||||
|
|--------|-------------|---------------|----------------|-------|
|
||||||
|
| 未实名卡 | 30秒 | - | - | 10 (高) |
|
||||||
|
| 已实名未激活 | - | - | - | - |
|
||||||
|
| 已激活卡(正常) | - | 60秒 | 60秒 | 20 (中) |
|
||||||
|
| 已激活卡(套餐即将用完) | - | 30秒 | 30秒 | 15 (高) |
|
||||||
|
| 已停用卡 | - | - | - | - |
|
||||||
|
|
||||||
|
### 动态调整策略
|
||||||
|
|
||||||
|
根据卡的流量使用情况动态调整轮询间隔:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
func calculateCheckInterval(packageUsage *PackageUsage) int {
|
||||||
|
usagePercent := float64(packageUsage.DataUsageMB) / float64(packageUsage.DataLimitMB)
|
||||||
|
|
||||||
|
if usagePercent >= 0.9 {
|
||||||
|
return 30 // 90%以上,30秒检查一次
|
||||||
|
} else if usagePercent >= 0.7 {
|
||||||
|
return 60 // 70-90%,60秒检查一次
|
||||||
|
} else {
|
||||||
|
return 180 // 70%以下,180秒检查一次
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 轮询开关控制
|
||||||
|
|
||||||
|
### 全局开关
|
||||||
|
|
||||||
|
IoT 卡的 `enable_polling` 字段控制是否参与轮询:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 禁用某张卡的轮询
|
||||||
|
UPDATE iot_cards SET enable_polling = false WHERE iccid = '89860123456789012345';
|
||||||
|
|
||||||
|
-- 启用某张卡的轮询
|
||||||
|
UPDATE iot_cards SET enable_polling = true WHERE iccid = '89860123456789012345';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置开关
|
||||||
|
|
||||||
|
轮询配置表的 `status` 字段控制整个配置是否启用:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 禁用某个轮询配置
|
||||||
|
UPDATE polling_configs SET status = 2 WHERE config_name = '未实名卡-快速轮询';
|
||||||
|
|
||||||
|
-- 启用某个轮询配置
|
||||||
|
UPDATE polling_configs SET status = 1 WHERE config_name = '未实名卡-快速轮询';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 单项开关
|
||||||
|
|
||||||
|
轮询配置表的 `*_check_enabled` 字段控制具体检查类型:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 只启用实名检查,禁用流量检查
|
||||||
|
UPDATE polling_configs
|
||||||
|
SET real_name_check_enabled = true,
|
||||||
|
card_data_check_enabled = false,
|
||||||
|
package_check_enabled = false
|
||||||
|
WHERE config_name = '未实名卡-快速轮询';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理和重试
|
||||||
|
|
||||||
|
### 错误处理策略
|
||||||
|
|
||||||
|
轮询任务可能因为以下原因失败:
|
||||||
|
1. 运营商 API 超时
|
||||||
|
2. 运营商 API 返回错误
|
||||||
|
3. 数据库连接失败
|
||||||
|
4. 网络故障
|
||||||
|
|
||||||
|
使用 Asynq 的重试机制:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
task := asynq.NewTask("iot:realname:check", payload)
|
||||||
|
|
||||||
|
// 设置重试策略
|
||||||
|
opts := []asynq.Option{
|
||||||
|
asynq.MaxRetry(3), // 最多重试 3 次
|
||||||
|
asynq.Timeout(30 * time.Second), // 任务超时时间 30 秒
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.Enqueue(task, opts...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 失败日志记录
|
||||||
|
|
||||||
|
记录失败的轮询任务:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
type PollingLog struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
TaskType string // realname/carddata/package
|
||||||
|
CardID uint
|
||||||
|
ConfigID uint
|
||||||
|
Success bool
|
||||||
|
ErrorMsg string
|
||||||
|
ExecutedAt time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func logPollingResult(taskType string, cardID, configID uint, err error) {
|
||||||
|
log := PollingLog{
|
||||||
|
TaskType: taskType,
|
||||||
|
CardID: cardID,
|
||||||
|
ConfigID: configID,
|
||||||
|
Success: err == nil,
|
||||||
|
ErrorMsg: fmt.Sprintf("%v", err),
|
||||||
|
ExecutedAt: time.Now(),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
db.Create(&log)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 监控和告警
|
||||||
|
|
||||||
|
### 监控指标
|
||||||
|
|
||||||
|
1. **轮询任务执行成功率**
|
||||||
|
- 实名检查成功率
|
||||||
|
- 卡流量检查成功率
|
||||||
|
- 套餐流量检查成功率
|
||||||
|
|
||||||
|
2. **轮询任务延迟**
|
||||||
|
- 任务入队时间到执行时间的延迟
|
||||||
|
- 平均延迟、P95、P99
|
||||||
|
|
||||||
|
3. **运营商 API 调用统计**
|
||||||
|
- 每分钟 API 调用次数
|
||||||
|
- API 响应时间
|
||||||
|
- API 错误率
|
||||||
|
|
||||||
|
4. **卡状态统计**
|
||||||
|
- 未实名卡数量
|
||||||
|
- 已激活卡数量
|
||||||
|
- 已停用卡数量
|
||||||
|
|
||||||
|
### 告警规则
|
||||||
|
|
||||||
|
1. **高失败率告警**
|
||||||
|
- 如果某类轮询任务 5 分钟内失败率超过 50%,触发告警
|
||||||
|
|
||||||
|
2. **高延迟告警**
|
||||||
|
- 如果轮询任务延迟超过 5 分钟,触发告警
|
||||||
|
|
||||||
|
3. **API 异常告警**
|
||||||
|
- 如果运营商 API 连续失败 10 次,触发告警
|
||||||
|
|
||||||
|
4. **流量异常告警**
|
||||||
|
- 如果某张卡流量使用突增(1小时内增加超过 100MB),触发告警
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 合理设置轮询间隔
|
||||||
|
|
||||||
|
- **频繁轮询的代价**: 增加运营商 API 调用次数,可能触发限流
|
||||||
|
- **稀疏轮询的风险**: 流量超额检测不及时,可能导致停机延迟
|
||||||
|
- **建议**: 根据业务需求和运营商 API 限制平衡轮询频率
|
||||||
|
|
||||||
|
### 2. 使用批量查询
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 不推荐: 逐个查询
|
||||||
|
for _, card := range cards {
|
||||||
|
usage := gateway.GetDataUsage(card.ICCID)
|
||||||
|
card.DataUsageMB = usage.TotalMB
|
||||||
|
db.Save(&card)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推荐: 批量查询
|
||||||
|
iccids := []string{}
|
||||||
|
for _, card := range cards {
|
||||||
|
iccids = append(iccids, card.ICCID)
|
||||||
|
}
|
||||||
|
usages := gateway.BatchGetDataUsage(iccids) // 批量查询
|
||||||
|
for _, card := range cards {
|
||||||
|
card.DataUsageMB = usages[card.ICCID]
|
||||||
|
}
|
||||||
|
db.Save(&cards) // 批量更新
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 实现幂等性
|
||||||
|
|
||||||
|
轮询任务可能会重复执行,必须保证幂等性:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
func ProcessRealNameCheck(cardID uint) error {
|
||||||
|
// 加锁,防止重复执行
|
||||||
|
lockKey := fmt.Sprintf("iot:realname:lock:%d", cardID)
|
||||||
|
lock := redis.SetNX(lockKey, "1", 60*time.Second)
|
||||||
|
if !lock {
|
||||||
|
return errors.New("task already running")
|
||||||
|
}
|
||||||
|
defer redis.Del(lockKey)
|
||||||
|
|
||||||
|
// 执行检查
|
||||||
|
card := db.FindIotCardByID(cardID)
|
||||||
|
result := gateway.CheckRealName(card.ICCID)
|
||||||
|
card.RealNameStatus = result.Status
|
||||||
|
card.LastRealNameCheckAt = time.Now()
|
||||||
|
db.Save(&card)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 记录详细日志
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 伪代码
|
||||||
|
logger.Info("开始实名检查",
|
||||||
|
zap.Uint("card_id", card.ID),
|
||||||
|
zap.String("iccid", card.ICCID),
|
||||||
|
zap.Uint("config_id", config.ID),
|
||||||
|
)
|
||||||
|
|
||||||
|
result, err := gateway.CheckRealName(card.ICCID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("实名检查失败",
|
||||||
|
zap.Uint("card_id", card.ID),
|
||||||
|
zap.String("iccid", card.ICCID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("实名检查成功",
|
||||||
|
zap.Uint("card_id", card.ID),
|
||||||
|
zap.String("iccid", card.ICCID),
|
||||||
|
zap.Int("real_name_status", result.Status),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
IoT SIM 管理系统的轮询机制具有以下特点:
|
||||||
|
|
||||||
|
1. **三层轮询体系**: 实名检查、卡流量检查、套餐流量检查相互独立
|
||||||
|
2. **灵活配置**: 支持按卡状态、运营商、优先级配置不同的轮询策略
|
||||||
|
3. **异步任务队列**: 使用 Asynq 实现高并发、可重试的任务处理
|
||||||
|
4. **梯度策略**: 支持根据流量使用情况动态调整轮询间隔
|
||||||
|
5. **开关控制**: 支持全局、配置、单项的轮询开关
|
||||||
|
6. **错误处理**: 完善的重试机制和错误日志记录
|
||||||
|
7. **监控告警**: 实时监控轮询任务执行情况和运营商 API 调用
|
||||||
|
|
||||||
|
通过合理配置和使用轮询机制,可以实现 IoT 卡的自动化管理,提高运营效率,降低人工成本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**最后更新**: 2026-01-12
|
||||||
|
**维护人员**: Claude Sonnet 4.5
|
||||||
2
go.mod
2
go.mod
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/golang-migrate/migrate/v4 v4.19.0
|
github.com/golang-migrate/migrate/v4 v4.19.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hibiken/asynq v0.25.1
|
github.com/hibiken/asynq v0.25.1
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
github.com/redis/go-redis/v9 v9.16.0
|
github.com/redis/go-redis/v9 v9.16.0
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
@@ -71,7 +72,6 @@ require (
|
|||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
github.com/magiconair/properties v1.8.10 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
|||||||
20
internal/iot/model/carrier.go
Normal file
20
internal/iot/model/carrier.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Carrier 运营商模型
|
||||||
|
// 存储运营商基础信息(中国移动、中国联通、中国电信)
|
||||||
|
type Carrier struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:运营商ID" json:"id"`
|
||||||
|
CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex;not null;comment:运营商编码(CMCC/CUCC/CTCC)" json:"carrier_code"`
|
||||||
|
CarrierName string `gorm:"column:carrier_name;type:varchar(100);not null;comment:运营商名称(中国移动/中国联通/中国电信)" json:"carrier_name"`
|
||||||
|
Description string `gorm:"column:description;type:varchar(500);comment:运营商描述" json:"description"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (Carrier) TableName() string {
|
||||||
|
return "carriers"
|
||||||
|
}
|
||||||
164
internal/iot/model/commission.go
Normal file
164
internal/iot/model/commission.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// AgentHierarchy 代理层级关系模型
|
||||||
|
// 树形代理关系(每个代理只有一个上级)
|
||||||
|
type AgentHierarchy struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:代理层级ID" json:"id"`
|
||||||
|
AgentID uint `gorm:"column:agent_id;type:bigint;uniqueIndex;not null;comment:代理用户ID" json:"agent_id"`
|
||||||
|
ParentAgentID uint `gorm:"column:parent_agent_id;type:bigint;comment:上级代理用户ID(NULL表示顶级代理)" json:"parent_agent_id"`
|
||||||
|
Level int `gorm:"column:level;type:int;not null;comment:代理层级(1, 2, 3...)" json:"level"`
|
||||||
|
Path string `gorm:"column:path;type:varchar(500);comment:代理路径(如: 1/5/12)" json:"path"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (AgentHierarchy) TableName() string {
|
||||||
|
return "agent_hierarchies"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommissionRule 分佣规则模型
|
||||||
|
// 三种分佣类型:一次性/长期/组合
|
||||||
|
type CommissionRule struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:分佣规则ID" json:"id"`
|
||||||
|
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
|
||||||
|
BusinessType string `gorm:"column:business_type;type:varchar(50);not null;comment:业务类型" json:"business_type"`
|
||||||
|
CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型 number_card-号卡 iot_card-IoT卡" json:"card_type"`
|
||||||
|
SeriesID uint `gorm:"column:series_id;type:bigint;comment:套餐系列ID(一次性分佣时用)" json:"series_id"`
|
||||||
|
PackageID uint `gorm:"column:package_id;type:bigint;comment:套餐ID(长期分佣时用)" json:"package_id"`
|
||||||
|
CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期 combined-组合" json:"commission_type"`
|
||||||
|
CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"`
|
||||||
|
CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"`
|
||||||
|
UnfreezeDays int `gorm:"column:unfreeze_days;type:int;default:0;comment:解冻天数" json:"unfreeze_days"`
|
||||||
|
MinActivationForUnfreeze int `gorm:"column:min_activation_for_unfreeze;type:int;default:0;comment:解冻最小激活量" json:"min_activation_for_unfreeze"`
|
||||||
|
ApprovalType string `gorm:"column:approval_type;type:varchar(20);default:'auto';comment:审批类型 auto-自动 manual-人工" json:"approval_type"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CommissionRule) TableName() string {
|
||||||
|
return "commission_rules"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommissionLadder 阶梯分佣配置模型
|
||||||
|
// 支持按激活量、提货量、充值量设置阶梯佣金
|
||||||
|
type CommissionLadder struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:阶梯分佣配置ID" json:"id"`
|
||||||
|
RuleID uint `gorm:"column:rule_id;type:bigint;not null;comment:分佣规则ID" json:"rule_id"`
|
||||||
|
LadderType string `gorm:"column:ladder_type;type:varchar(50);not null;comment:阶梯类型 activation-激活量 pickup-提货量 deposit-充值量" json:"ladder_type"`
|
||||||
|
ThresholdValue int `gorm:"column:threshold_value;type:int;not null;comment:阈值" json:"threshold_value"`
|
||||||
|
CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"`
|
||||||
|
CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CommissionLadder) TableName() string {
|
||||||
|
return "commission_ladder"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommissionCombinedCondition 组合分佣条件模型
|
||||||
|
// 支持时间点 OR 套餐周期阈值的 OR 条件解冻
|
||||||
|
type CommissionCombinedCondition struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:组合分佣条件ID" json:"id"`
|
||||||
|
RuleID uint `gorm:"column:rule_id;type:bigint;uniqueIndex;not null;comment:分佣规则ID" json:"rule_id"`
|
||||||
|
OneTimeCommissionMode string `gorm:"column:one_time_commission_mode;type:varchar(20);comment:一次性分佣模式 fixed-固定金额 percent-百分比" json:"one_time_commission_mode"`
|
||||||
|
OneTimeCommissionValue float64 `gorm:"column:one_time_commission_value;type:decimal(10,2);comment:一次性分佣值" json:"one_time_commission_value"`
|
||||||
|
LongTermCommissionMode string `gorm:"column:long_term_commission_mode;type:varchar(20);comment:长期分佣模式 fixed-固定金额 percent-百分比" json:"long_term_commission_mode"`
|
||||||
|
LongTermCommissionValue float64 `gorm:"column:long_term_commission_value;type:decimal(10,2);comment:长期分佣值" json:"long_term_commission_value"`
|
||||||
|
LongTermTriggerTimePoint *time.Time `gorm:"column:long_term_trigger_time_point;comment:长期分佣触发时间点(如实名后3个月)" json:"long_term_trigger_time_point"`
|
||||||
|
LongTermTriggerPackageCycles int `gorm:"column:long_term_trigger_package_cycles;type:int;comment:长期分佣触发套餐周期数(如10个套餐周期)" json:"long_term_trigger_package_cycles"`
|
||||||
|
LongTermTriggerNetworkMonths int `gorm:"column:long_term_trigger_network_months;type:int;comment:长期分佣触发在网月数(号卡专用)" json:"long_term_trigger_network_months"`
|
||||||
|
LongTermUnfreezeDays int `gorm:"column:long_term_unfreeze_days;type:int;default:0;comment:长期分佣解冻天数" json:"long_term_unfreeze_days"`
|
||||||
|
LongTermMinActivation int `gorm:"column:long_term_min_activation;type:int;default:0;comment:长期分佣解冻最小激活量" json:"long_term_min_activation"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CommissionCombinedCondition) TableName() string {
|
||||||
|
return "commission_combined_conditions"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommissionRecord 分佣记录模型
|
||||||
|
// 记录分佣的冻结、解冻、发放状态
|
||||||
|
type CommissionRecord struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:分佣记录ID" json:"id"`
|
||||||
|
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
|
||||||
|
OrderID uint `gorm:"column:order_id;type:bigint;not null;comment:订单ID" json:"order_id"`
|
||||||
|
RuleID uint `gorm:"column:rule_id;type:bigint;not null;comment:分佣规则ID" json:"rule_id"`
|
||||||
|
CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期" json:"commission_type"`
|
||||||
|
Amount float64 `gorm:"column:amount;type:decimal(10,2);not null;comment:分佣金额(元)" json:"amount"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-已冻结 2-解冻中 3-已发放 4-已失效" json:"status"`
|
||||||
|
UnfrozenAt *time.Time `gorm:"column:unfrozen_at;comment:解冻时间" json:"unfrozen_at"`
|
||||||
|
ReleasedAt *time.Time `gorm:"column:released_at;comment:发放时间" json:"released_at"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CommissionRecord) TableName() string {
|
||||||
|
return "commission_records"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommissionApproval 分佣审批模型
|
||||||
|
// 分佣解冻审批流程
|
||||||
|
type CommissionApproval struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:分佣审批ID" json:"id"`
|
||||||
|
CommissionRecordID uint `gorm:"column:commission_record_id;type:bigint;not null;comment:分佣记录ID" json:"commission_record_id"`
|
||||||
|
ApproverID uint `gorm:"column:approver_id;type:bigint;comment:审批人用户ID" json:"approver_id"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待审批 2-已通过 3-已拒绝" json:"status"`
|
||||||
|
Reason string `gorm:"column:reason;type:text;comment:原因" json:"reason"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CommissionApproval) TableName() string {
|
||||||
|
return "commission_approvals"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommissionTemplate 分佣模板模型
|
||||||
|
// 创建和管理分佣模板,快速为代理分配产品时设置佣金规则
|
||||||
|
type CommissionTemplate struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:分佣模板ID" json:"id"`
|
||||||
|
TemplateName string `gorm:"column:template_name;type:varchar(255);uniqueIndex;not null;comment:模板名称" json:"template_name"`
|
||||||
|
BusinessType string `gorm:"column:business_type;type:varchar(50);not null;comment:业务类型" json:"business_type"`
|
||||||
|
CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型 number_card-号卡 iot_card-IoT卡" json:"card_type"`
|
||||||
|
CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期 combined-组合" json:"commission_type"`
|
||||||
|
CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"`
|
||||||
|
CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"`
|
||||||
|
UnfreezeDays int `gorm:"column:unfreeze_days;type:int;default:0;comment:解冻天数" json:"unfreeze_days"`
|
||||||
|
MinActivationForUnfreeze int `gorm:"column:min_activation_for_unfreeze;type:int;default:0;comment:解冻最小激活量" json:"min_activation_for_unfreeze"`
|
||||||
|
ApprovalType string `gorm:"column:approval_type;type:varchar(20);default:'auto';comment:审批类型 auto-自动 manual-人工" json:"approval_type"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CommissionTemplate) TableName() string {
|
||||||
|
return "commission_templates"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CarrierSettlement 号卡运营商结算模型
|
||||||
|
// 运营商周期性结算的佣金总额,再分配给代理
|
||||||
|
type CarrierSettlement struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:运营商结算ID" json:"id"`
|
||||||
|
CommissionRecordID uint `gorm:"column:commission_record_id;type:bigint;uniqueIndex;not null;comment:分佣记录ID" json:"commission_record_id"`
|
||||||
|
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
|
||||||
|
SettlementMonth string `gorm:"column:settlement_month;type:varchar(20);not null;comment:结算月份(如 2026-01)" json:"settlement_month"`
|
||||||
|
SettlementAmount float64 `gorm:"column:settlement_amount;type:decimal(18,2);not null;comment:结算金额(元)" json:"settlement_amount"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待结算 2-已结算" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CarrierSettlement) TableName() string {
|
||||||
|
return "carrier_settlements"
|
||||||
|
}
|
||||||
20
internal/iot/model/data_usage.go
Normal file
20
internal/iot/model/data_usage.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// DataUsageRecord 流量使用记录模型
|
||||||
|
// 记录卡的流量历史,支持流量查询和分析
|
||||||
|
type DataUsageRecord struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
|
||||||
|
IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;comment:IoT卡ID" json:"iot_card_id"`
|
||||||
|
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;not null;comment:流量使用量(MB)" json:"data_usage_mb"`
|
||||||
|
DataIncreaseMB int64 `gorm:"column:data_increase_mb;type:bigint;default:0;comment:相比上次的增量(MB)" json:"data_increase_mb"`
|
||||||
|
CheckTime time.Time `gorm:"column:check_time;not null;comment:检查时间" json:"check_time"`
|
||||||
|
Source string `gorm:"column:source;type:varchar(50);default:'polling';comment:数据来源 polling-轮询 manual-手动 gateway-回调" json:"source"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (DataUsageRecord) TableName() string {
|
||||||
|
return "data_usage_records"
|
||||||
|
}
|
||||||
31
internal/iot/model/device.go
Normal file
31
internal/iot/model/device.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Device 设备模型
|
||||||
|
// 用户的物联网设备(如 GPS 追踪器、智能传感器)
|
||||||
|
// 可绑定 1-4 张 IoT 卡,主要用于批量管理和设备操作
|
||||||
|
type Device struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:设备ID" json:"id"`
|
||||||
|
DeviceNo string `gorm:"column:device_no;type:varchar(100);uniqueIndex;not null;comment:设备编号(唯一标识)" json:"device_no"`
|
||||||
|
DeviceName string `gorm:"column:device_name;type:varchar(255);comment:设备名称" json:"device_name"`
|
||||||
|
DeviceModel string `gorm:"column:device_model;type:varchar(100);comment:设备型号" json:"device_model"`
|
||||||
|
DeviceType string `gorm:"column:device_type;type:varchar(50);comment:设备类型" json:"device_type"`
|
||||||
|
MaxSimSlots int `gorm:"column:max_sim_slots;type:int;default:4;comment:最大插槽数量(默认4)" json:"max_sim_slots"`
|
||||||
|
Manufacturer string `gorm:"column:manufacturer;type:varchar(255);comment:制造商" json:"manufacturer"`
|
||||||
|
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||||
|
OwnerType string `gorm:"column:owner_type;type:varchar(20);default:'platform';not null;comment:所有者类型 platform-平台 agent-代理 user-用户" json:"owner_type"`
|
||||||
|
OwnerID uint `gorm:"column:owner_id;type:bigint;default:0;not null;comment:所有者ID" json:"owner_id"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
|
||||||
|
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
|
||||||
|
DeviceUsername string `gorm:"column:device_username;type:varchar(100);comment:设备登录用户名" json:"device_username"`
|
||||||
|
DevicePasswordEncrypted string `gorm:"column:device_password_encrypted;type:varchar(255);comment:设备登录密码(加密)" json:"device_password_encrypted"`
|
||||||
|
DeviceAPIEndpoint string `gorm:"column:device_api_endpoint;type:varchar(500);comment:设备API端点" json:"device_api_endpoint"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (Device) TableName() string {
|
||||||
|
return "devices"
|
||||||
|
}
|
||||||
70
internal/iot/model/financial.go
Normal file
70
internal/iot/model/financial.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommissionWithdrawalRequest 佣金提现申请模型
|
||||||
|
// 代理佣金提现申请、审批流程、提现记录查询
|
||||||
|
type CommissionWithdrawalRequest struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:提现申请ID" json:"id"`
|
||||||
|
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
|
||||||
|
Amount float64 `gorm:"column:amount;type:decimal(18,2);not null;comment:提现金额(元)" json:"amount"`
|
||||||
|
Fee float64 `gorm:"column:fee;type:decimal(18,2);default:0;comment:手续费(元)" json:"fee"`
|
||||||
|
ActualAmount float64 `gorm:"column:actual_amount;type:decimal(18,2);comment:实际到账金额(元)" json:"actual_amount"`
|
||||||
|
WithdrawalMethod string `gorm:"column:withdrawal_method;type:varchar(20);comment:提现方式 alipay-支付宝 wechat-微信 bank-银行卡" json:"withdrawal_method"`
|
||||||
|
AccountInfo pq.StringArray `gorm:"column:account_info;type:jsonb;comment:收款账户信息(姓名、账号等)" json:"account_info"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-待审核 2-已通过 3-已拒绝 4-已到账" json:"status"`
|
||||||
|
ApprovedBy uint `gorm:"column:approved_by;type:bigint;comment:审批人用户ID" json:"approved_by"`
|
||||||
|
ApprovedAt *time.Time `gorm:"column:approved_at;comment:审批时间" json:"approved_at"`
|
||||||
|
PaidAt *time.Time `gorm:"column:paid_at;comment:到账时间" json:"paid_at"`
|
||||||
|
RejectReason string `gorm:"column:reject_reason;type:text;comment:拒绝原因" json:"reject_reason"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CommissionWithdrawalRequest) TableName() string {
|
||||||
|
return "commission_withdrawal_requests"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommissionWithdrawalSetting 佣金提现设置模型
|
||||||
|
// 提现参数配置(最低金额、手续费率、到账时间等)
|
||||||
|
type CommissionWithdrawalSetting struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:提现设置ID" json:"id"`
|
||||||
|
MinWithdrawalAmount float64 `gorm:"column:min_withdrawal_amount;type:decimal(10,2);comment:最低提现金额(元)" json:"min_withdrawal_amount"`
|
||||||
|
FeeRate float64 `gorm:"column:fee_rate;type:decimal(5,4);comment:手续费率(如 0.01 表示 1%)" json:"fee_rate"`
|
||||||
|
ArrivalDays int `gorm:"column:arrival_days;type:int;comment:到账天数" json:"arrival_days"`
|
||||||
|
IsActive bool `gorm:"column:is_active;type:boolean;default:true;comment:是否生效(最新一条)" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CommissionWithdrawalSetting) TableName() string {
|
||||||
|
return "commission_withdrawal_settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentMerchantSetting 收款商户设置模型
|
||||||
|
// 配置支付参数(支付宝、微信等收款账户)
|
||||||
|
type PaymentMerchantSetting struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:收款商户ID" json:"id"`
|
||||||
|
UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID" json:"user_id"`
|
||||||
|
MerchantType string `gorm:"column:merchant_type;type:varchar(20);comment:商户类型 alipay-支付宝 wechat-微信 bank-银行卡" json:"merchant_type"`
|
||||||
|
AccountName string `gorm:"column:account_name;type:varchar(255);comment:账户名称" json:"account_name"`
|
||||||
|
AccountNumber string `gorm:"column:account_number;type:varchar(255);comment:账号" json:"account_number"`
|
||||||
|
BankName string `gorm:"column:bank_name;type:varchar(255);comment:银行名称(仅银行卡)" json:"bank_name"`
|
||||||
|
BankBranch string `gorm:"column:bank_branch;type:varchar(255);comment:开户行(仅银行卡)" json:"bank_branch"`
|
||||||
|
IsVerified bool `gorm:"column:is_verified;type:boolean;default:false;comment:是否已验证" json:"is_verified"`
|
||||||
|
IsDefault bool `gorm:"column:is_default;type:boolean;default:false;comment:是否默认账户" json:"is_default"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (PaymentMerchantSetting) TableName() string {
|
||||||
|
return "payment_merchant_settings"
|
||||||
|
}
|
||||||
39
internal/iot/model/iot_card.go
Normal file
39
internal/iot/model/iot_card.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// IotCard IoT 卡模型
|
||||||
|
// 物联网卡/流量卡的统一管理实体
|
||||||
|
// 支持平台自营、代理分销、用户购买等所有权模式
|
||||||
|
type IotCard struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:IoT 卡 ID" json:"id"`
|
||||||
|
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex;not null;comment:ICCID(唯一标识)" json:"iccid"`
|
||||||
|
CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型" json:"card_type"`
|
||||||
|
CardCategory string `gorm:"column:card_category;type:varchar(20);default:'normal';not null;comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
|
||||||
|
CarrierID uint `gorm:"column:carrier_id;type:bigint;not null;comment:运营商ID" json:"carrier_id"`
|
||||||
|
IMSI string `gorm:"column:imsi;type:varchar(50);comment:IMSI" json:"imsi"`
|
||||||
|
MSISDN string `gorm:"column:msisdn;type:varchar(20);comment:MSISDN(手机号码)" json:"msisdn"`
|
||||||
|
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||||
|
Supplier string `gorm:"column:supplier;type:varchar(255);comment:供应商" json:"supplier"`
|
||||||
|
CostPrice float64 `gorm:"column:cost_price;type:decimal(10,2);default:0;comment:成本价(元)" json:"cost_price"`
|
||||||
|
DistributePrice float64 `gorm:"column:distribute_price;type:decimal(10,2);default:0;comment:分销价(元)" json:"distribute_price"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
|
||||||
|
OwnerType string `gorm:"column:owner_type;type:varchar(20);default:'platform';not null;comment:所有者类型 platform-平台 agent-代理 user-用户 device-设备" json:"owner_type"`
|
||||||
|
OwnerID uint `gorm:"column:owner_id;type:bigint;default:0;not null;comment:所有者ID" json:"owner_id"`
|
||||||
|
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
|
||||||
|
ActivationStatus int `gorm:"column:activation_status;type:int;default:0;not null;comment:激活状态 0-未激活 1-已激活" json:"activation_status"`
|
||||||
|
RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;not null;comment:实名状态 0-未实名 1-已实名(行业卡可以保持0)" json:"real_name_status"`
|
||||||
|
NetworkStatus int `gorm:"column:network_status;type:int;default:0;not null;comment:网络状态 0-停机 1-开机" json:"network_status"`
|
||||||
|
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:累计流量使用(MB)" json:"data_usage_mb"`
|
||||||
|
EnablePolling bool `gorm:"column:enable_polling;type:boolean;default:true;comment:是否参与轮询 true-参与 false-不参与" json:"enable_polling"`
|
||||||
|
LastDataCheckAt *time.Time `gorm:"column:last_data_check_at;comment:最后一次流量检查时间" json:"last_data_check_at"`
|
||||||
|
LastRealNameCheckAt *time.Time `gorm:"column:last_real_name_check_at;comment:最后一次实名检查时间" json:"last_real_name_check_at"`
|
||||||
|
LastSyncTime *time.Time `gorm:"column:last_sync_time;comment:最后一次与Gateway同步时间" json:"last_sync_time"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (IotCard) TableName() string {
|
||||||
|
return "iot_cards"
|
||||||
|
}
|
||||||
25
internal/iot/model/number_card.go
Normal file
25
internal/iot/model/number_card.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// NumberCard 号卡模型
|
||||||
|
// 完全独立的业务线,从上游平台下单
|
||||||
|
// 使用虚拟商品编码映射运营商订单
|
||||||
|
type NumberCard struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:号卡ID" json:"id"`
|
||||||
|
VirtualProductCode string `gorm:"column:virtual_product_code;type:varchar(100);uniqueIndex;not null;comment:虚拟商品编码(用于对应运营商订单)" json:"virtual_product_code"`
|
||||||
|
CardName string `gorm:"column:card_name;type:varchar(255);not null;comment:号卡名称" json:"card_name"`
|
||||||
|
CardType string `gorm:"column:card_type;type:varchar(50);comment:号卡类型" json:"card_type"`
|
||||||
|
Carrier string `gorm:"column:carrier;type:varchar(50);comment:运营商" json:"carrier"`
|
||||||
|
DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;comment:流量额度(MB)" json:"data_amount_mb"`
|
||||||
|
Price float64 `gorm:"column:price;type:decimal(10,2);comment:价格(元)" json:"price"`
|
||||||
|
AgentID uint `gorm:"column:agent_id;type:bigint;comment:代理用户ID" json:"agent_id"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在售 2-下架" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (NumberCard) TableName() string {
|
||||||
|
return "number_cards"
|
||||||
|
}
|
||||||
35
internal/iot/model/order.go
Normal file
35
internal/iot/model/order.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Order 订单模型
|
||||||
|
// 支持两种订单类型:套餐订单(单卡/设备级)、号卡订单
|
||||||
|
type Order struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:订单ID" json:"id"`
|
||||||
|
OrderNo string `gorm:"column:order_no;type:varchar(100);uniqueIndex;not null;comment:订单号(唯一标识)" json:"order_no"`
|
||||||
|
OrderType int `gorm:"column:order_type;type:int;not null;comment:订单类型 1-套餐订单 2-号卡订单" json:"order_type"`
|
||||||
|
IotCardID uint `gorm:"column:iot_card_id;type:bigint;comment:IoT卡ID(单卡套餐订单时有值)" json:"iot_card_id"`
|
||||||
|
DeviceID uint `gorm:"column:device_id;type:bigint;comment:设备ID(设备级套餐订单时有值)" json:"device_id"`
|
||||||
|
NumberCardID uint `gorm:"column:number_card_id;type:bigint;comment:号卡ID(号卡订单时有值)" json:"number_card_id"`
|
||||||
|
PackageID uint `gorm:"column:package_id;type:bigint;comment:套餐ID(套餐订单时有值)" json:"package_id"`
|
||||||
|
UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID" json:"user_id"`
|
||||||
|
AgentID uint `gorm:"column:agent_id;type:bigint;comment:代理用户ID" json:"agent_id"`
|
||||||
|
Amount float64 `gorm:"column:amount;type:decimal(10,2);not null;comment:订单金额(元)" json:"amount"`
|
||||||
|
PaymentMethod string `gorm:"column:payment_method;type:varchar(20);comment:支付方式 wallet-钱包 online-在线支付 carrier-运营商支付" json:"payment_method"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待支付 2-已支付 3-已完成 4-已取消 5-已退款" json:"status"`
|
||||||
|
CarrierOrderID string `gorm:"column:carrier_order_id;type:varchar(255);comment:运营商订单ID" json:"carrier_order_id"`
|
||||||
|
CarrierOrderData pq.StringArray `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据(JSON)" json:"carrier_order_data"`
|
||||||
|
PaidAt *time.Time `gorm:"column:paid_at;comment:支付时间" json:"paid_at"`
|
||||||
|
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (Order) TableName() string {
|
||||||
|
return "orders"
|
||||||
|
}
|
||||||
107
internal/iot/model/package.go
Normal file
107
internal/iot/model/package.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// PackageSeries 套餐系列模型
|
||||||
|
// 套餐的分组,用于一次性分佣规则配置
|
||||||
|
type PackageSeries struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:套餐系列ID" json:"id"`
|
||||||
|
SeriesCode string `gorm:"column:series_code;type:varchar(100);uniqueIndex;not null;comment:系列编码" json:"series_code"`
|
||||||
|
SeriesName string `gorm:"column:series_name;type:varchar(255);not null;comment:系列名称" json:"series_name"`
|
||||||
|
Description string `gorm:"column:description;type:text;comment:描述" json:"description"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (PackageSeries) TableName() string {
|
||||||
|
return "package_series"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package 套餐模型
|
||||||
|
// 只适用于 IoT 卡,支持真流量/虚流量共存机制
|
||||||
|
type Package struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:套餐ID" json:"id"`
|
||||||
|
PackageCode string `gorm:"column:package_code;type:varchar(100);uniqueIndex;not null;comment:套餐编码" json:"package_code"`
|
||||||
|
PackageName string `gorm:"column:package_name;type:varchar(255);not null;comment:套餐名称" json:"package_name"`
|
||||||
|
SeriesID uint `gorm:"column:series_id;type:bigint;comment:套餐系列ID" json:"series_id"`
|
||||||
|
PackageType string `gorm:"column:package_type;type:varchar(50);not null;comment:套餐类型 formal-正式套餐 addon-附加套餐" json:"package_type"`
|
||||||
|
DurationMonths int `gorm:"column:duration_months;type:int;not null;comment:套餐时长(月数) 1-月套餐 12-年套餐" json:"duration_months"`
|
||||||
|
DataType string `gorm:"column:data_type;type:varchar(20);comment:流量类型 real-真流量 virtual-虚流量" json:"data_type"`
|
||||||
|
RealDataMB int64 `gorm:"column:real_data_mb;type:bigint;default:0;comment:真流量额度(MB)" json:"real_data_mb"`
|
||||||
|
VirtualDataMB int64 `gorm:"column:virtual_data_mb;type:bigint;default:0;comment:虚流量额度(MB,用于停机判断)" json:"virtual_data_mb"`
|
||||||
|
DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;default:0;comment:总流量额度(MB)" json:"data_amount_mb"`
|
||||||
|
Price float64 `gorm:"column:price;type:decimal(10,2);not null;comment:套餐价格(元)" json:"price"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (Package) TableName() string {
|
||||||
|
return "packages"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentPackageAllocation 代理套餐分配模型
|
||||||
|
// 为直属下级代理分配套餐,设置佣金模式
|
||||||
|
type AgentPackageAllocation struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:分配ID" json:"id"`
|
||||||
|
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
|
||||||
|
PackageID uint `gorm:"column:package_id;type:bigint;not null;comment:套餐ID" json:"package_id"`
|
||||||
|
CostPrice float64 `gorm:"column:cost_price;type:decimal(10,2);not null;comment:成本价(元)" json:"cost_price"`
|
||||||
|
RetailPrice float64 `gorm:"column:retail_price;type:decimal(10,2);not null;comment:零售价(元)" json:"retail_price"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (AgentPackageAllocation) TableName() string {
|
||||||
|
return "agent_package_allocations"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceSimBinding 设备-IoT卡绑定关系模型
|
||||||
|
// 管理设备与 IoT 卡的多对多绑定关系(1 设备绑定 1-4 张 IoT 卡)
|
||||||
|
type DeviceSimBinding struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:绑定ID" json:"id"`
|
||||||
|
DeviceID uint `gorm:"column:device_id;type:bigint;not null;comment:设备ID" json:"device_id"`
|
||||||
|
IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;comment:IoT卡ID" json:"iot_card_id"`
|
||||||
|
SlotPosition int `gorm:"column:slot_position;type:int;comment:插槽位置(1, 2, 3, 4)" json:"slot_position"`
|
||||||
|
BindStatus int `gorm:"column:bind_status;type:int;default:1;comment:绑定状态 1-已绑定 2-已解绑" json:"bind_status"`
|
||||||
|
BindTime *time.Time `gorm:"column:bind_time;comment:绑定时间" json:"bind_time"`
|
||||||
|
UnbindTime *time.Time `gorm:"column:unbind_time;comment:解绑时间" json:"unbind_time"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (DeviceSimBinding) TableName() string {
|
||||||
|
return "device_sim_bindings"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageUsage 套餐使用情况模型
|
||||||
|
// 跟踪单卡套餐和设备级套餐的流量使用
|
||||||
|
type PackageUsage struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:套餐使用ID" json:"id"`
|
||||||
|
OrderID uint `gorm:"column:order_id;type:bigint;not null;comment:订单ID" json:"order_id"`
|
||||||
|
PackageID uint `gorm:"column:package_id;type:bigint;not null;comment:套餐ID" json:"package_id"`
|
||||||
|
UsageType string `gorm:"column:usage_type;type:varchar(20);not null;comment:使用类型 single_card-单卡套餐 device-设备级套餐" json:"usage_type"`
|
||||||
|
IotCardID uint `gorm:"column:iot_card_id;type:bigint;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"`
|
||||||
|
DeviceID uint `gorm:"column:device_id;type:bigint;comment:设备ID(设备级套餐时有值)" json:"device_id"`
|
||||||
|
DataLimitMB int64 `gorm:"column:data_limit_mb;type:bigint;not null;comment:流量限额(MB)" json:"data_limit_mb"`
|
||||||
|
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:已使用流量(MB)" json:"data_usage_mb"`
|
||||||
|
RealDataUsageMB int64 `gorm:"column:real_data_usage_mb;type:bigint;default:0;comment:真流量使用(MB)" json:"real_data_usage_mb"`
|
||||||
|
VirtualDataUsageMB int64 `gorm:"column:virtual_data_usage_mb;type:bigint;default:0;comment:虚流量使用(MB)" json:"virtual_data_usage_mb"`
|
||||||
|
ActivatedAt time.Time `gorm:"column:activated_at;not null;comment:套餐生效时间" json:"activated_at"`
|
||||||
|
ExpiresAt time.Time `gorm:"column:expires_at;not null;comment:套餐过期时间" json:"expires_at"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-生效中 2-已用完 3-已过期" json:"status"`
|
||||||
|
LastPackageCheckAt *time.Time `gorm:"column:last_package_check_at;comment:最后一次套餐流量检查时间" json:"last_package_check_at"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (PackageUsage) TableName() string {
|
||||||
|
return "package_usages"
|
||||||
|
}
|
||||||
28
internal/iot/model/polling.go
Normal file
28
internal/iot/model/polling.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// PollingConfig 轮询配置模型
|
||||||
|
// 支持梯度轮询策略(实名检查、卡流量检查、套餐流量检查)
|
||||||
|
type PollingConfig struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:轮询配置ID" json:"id"`
|
||||||
|
ConfigName string `gorm:"column:config_name;type:varchar(100);uniqueIndex;not null;comment:配置名称(如 未实名卡、实名卡)" json:"config_name"`
|
||||||
|
Description string `gorm:"column:description;type:varchar(500);comment:配置描述" json:"description"`
|
||||||
|
CardCondition string `gorm:"column:card_condition;type:varchar(50);comment:卡状态条件 not_real_name-未实名 real_name-已实名 activated-已激活 suspended-已停用" json:"card_condition"`
|
||||||
|
CarrierID uint `gorm:"column:carrier_id;type:bigint;comment:运营商ID(NULL表示所有运营商)" json:"carrier_id"`
|
||||||
|
RealNameCheckEnabled bool `gorm:"column:real_name_check_enabled;type:boolean;default:false;comment:是否启用实名检查" json:"real_name_check_enabled"`
|
||||||
|
RealNameCheckInterval int `gorm:"column:real_name_check_interval;type:int;default:60;comment:实名检查间隔(秒)" json:"real_name_check_interval"`
|
||||||
|
CardDataCheckEnabled bool `gorm:"column:card_data_check_enabled;type:boolean;default:false;comment:是否启用卡流量检查" json:"card_data_check_enabled"`
|
||||||
|
CardDataCheckInterval int `gorm:"column:card_data_check_interval;type:int;default:60;comment:卡流量检查间隔(秒)" json:"card_data_check_interval"`
|
||||||
|
PackageCheckEnabled bool `gorm:"column:package_check_enabled;type:boolean;default:false;comment:是否启用套餐流量检查" json:"package_check_enabled"`
|
||||||
|
PackageCheckInterval int `gorm:"column:package_check_interval;type:int;default:60;comment:套餐流量检查间隔(秒)" json:"package_check_interval"`
|
||||||
|
Priority int `gorm:"column:priority;type:int;default:100;not null;comment:优先级(数字越小优先级越高)" json:"priority"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (PollingConfig) TableName() string {
|
||||||
|
return "polling_configs"
|
||||||
|
}
|
||||||
45
internal/iot/model/system.go
Normal file
45
internal/iot/model/system.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// DevCapabilityConfig 开发能力配置模型
|
||||||
|
// 管理 API 对接参数(AppID、AppSecret、回调地址等)
|
||||||
|
type DevCapabilityConfig struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:开发能力配置ID" json:"id"`
|
||||||
|
UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID(平台或代理)" json:"user_id"`
|
||||||
|
AppName string `gorm:"column:app_name;type:varchar(255);comment:应用名称" json:"app_name"`
|
||||||
|
AppID string `gorm:"column:app_id;type:varchar(100);uniqueIndex;comment:应用ID" json:"app_id"`
|
||||||
|
AppSecret string `gorm:"column:app_secret;type:varchar(255);comment:应用密钥" json:"app_secret"`
|
||||||
|
CallbackURL string `gorm:"column:callback_url;type:varchar(500);comment:回调地址" json:"callback_url"`
|
||||||
|
IPWhitelist string `gorm:"column:ip_whitelist;type:text;comment:IP白名单(多个IP用逗号分隔)" json:"ip_whitelist"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (DevCapabilityConfig) TableName() string {
|
||||||
|
return "dev_capability_configs"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardReplacementRequest 换卡申请模型
|
||||||
|
// 客户提交的换卡申请管理,处理换卡申请
|
||||||
|
type CardReplacementRequest struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:换卡申请ID" json:"id"`
|
||||||
|
UserID uint `gorm:"column:user_id;type:bigint;not null;comment:申请用户ID" json:"user_id"`
|
||||||
|
OldICCID string `gorm:"column:old_iccid;type:varchar(50);not null;comment:旧卡ICCID" json:"old_iccid"`
|
||||||
|
NewICCID string `gorm:"column:new_iccid;type:varchar(50);comment:新卡ICCID(审批时填充)" json:"new_iccid"`
|
||||||
|
Reason string `gorm:"column:reason;type:text;comment:换卡原因" json:"reason"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-待处理 2-已通过 3-已拒绝 4-已完成" json:"status"`
|
||||||
|
ApprovedBy uint `gorm:"column:approved_by;type:bigint;comment:处理人用户ID" json:"approved_by"`
|
||||||
|
ApprovedAt *time.Time `gorm:"column:approved_at;comment:处理时间" json:"approved_at"`
|
||||||
|
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间(新卡激活时间)" json:"completed_at"`
|
||||||
|
RejectReason string `gorm:"column:reject_reason;type:text;comment:拒绝原因" json:"reject_reason"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (CardReplacementRequest) TableName() string {
|
||||||
|
return "card_replacement_requests"
|
||||||
|
}
|
||||||
38
migrations/000005_create_iot_sim_management_tables.down.sql
Normal file
38
migrations/000005_create_iot_sim_management_tables.down.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- IoT SIM 管理系统数据表删除脚本
|
||||||
|
-- 创建时间: 2026-01-12
|
||||||
|
-- 说明: 回滚所有 IoT SIM 管理相关表
|
||||||
|
|
||||||
|
-- 删除系统管理表
|
||||||
|
DROP TABLE IF EXISTS card_replacement_requests;
|
||||||
|
DROP TABLE IF EXISTS dev_capability_configs;
|
||||||
|
|
||||||
|
-- 删除财务管理表
|
||||||
|
DROP TABLE IF EXISTS payment_merchant_settings;
|
||||||
|
DROP TABLE IF EXISTS commission_withdrawal_settings;
|
||||||
|
DROP TABLE IF EXISTS commission_withdrawal_requests;
|
||||||
|
|
||||||
|
-- 删除分佣相关表
|
||||||
|
DROP TABLE IF EXISTS carrier_settlements;
|
||||||
|
DROP TABLE IF EXISTS commission_templates;
|
||||||
|
DROP TABLE IF EXISTS commission_approvals;
|
||||||
|
DROP TABLE IF EXISTS commission_records;
|
||||||
|
DROP TABLE IF EXISTS commission_combined_conditions;
|
||||||
|
DROP TABLE IF EXISTS commission_ladder;
|
||||||
|
DROP TABLE IF EXISTS commission_rules;
|
||||||
|
DROP TABLE IF EXISTS agent_hierarchies;
|
||||||
|
|
||||||
|
-- 删除套餐和轮询相关表
|
||||||
|
DROP TABLE IF EXISTS data_usage_records;
|
||||||
|
DROP TABLE IF EXISTS polling_configs;
|
||||||
|
DROP TABLE IF EXISTS package_usages;
|
||||||
|
|
||||||
|
-- 删除核心业务表
|
||||||
|
DROP TABLE IF EXISTS orders;
|
||||||
|
DROP TABLE IF EXISTS device_sim_bindings;
|
||||||
|
DROP TABLE IF EXISTS agent_package_allocations;
|
||||||
|
DROP TABLE IF EXISTS packages;
|
||||||
|
DROP TABLE IF EXISTS package_series;
|
||||||
|
DROP TABLE IF EXISTS number_cards;
|
||||||
|
DROP TABLE IF EXISTS devices;
|
||||||
|
DROP TABLE IF EXISTS iot_cards;
|
||||||
|
DROP TABLE IF EXISTS carriers;
|
||||||
841
migrations/000005_create_iot_sim_management_tables.up.sql
Normal file
841
migrations/000005_create_iot_sim_management_tables.up.sql
Normal file
@@ -0,0 +1,841 @@
|
|||||||
|
-- IoT SIM 管理系统数据表创建脚本
|
||||||
|
-- 创建时间: 2026-01-12
|
||||||
|
-- 说明: 本脚本创建 IoT 卡、设备、号卡、套餐、订单、分佣等核心业务表
|
||||||
|
|
||||||
|
-- ========================================
|
||||||
|
-- 1. 核心业务表
|
||||||
|
-- ========================================
|
||||||
|
|
||||||
|
-- 1.1 运营商表
|
||||||
|
CREATE TABLE carriers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
carrier_code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
carrier_name VARCHAR(100) NOT NULL,
|
||||||
|
description VARCHAR(500),
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE carriers IS '运营商表';
|
||||||
|
COMMENT ON COLUMN carriers.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN carriers.carrier_code IS '运营商编码(CMCC/CUCC/CTCC)';
|
||||||
|
COMMENT ON COLUMN carriers.carrier_name IS '运营商名称(中国移动/中国联通/中国电信)';
|
||||||
|
COMMENT ON COLUMN carriers.description IS '运营商描述';
|
||||||
|
COMMENT ON COLUMN carriers.status IS '状态 1-启用 2-禁用';
|
||||||
|
COMMENT ON COLUMN carriers.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN carriers.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 插入初始数据
|
||||||
|
INSERT INTO carriers (carrier_code, carrier_name, status) VALUES
|
||||||
|
('CMCC', '中国移动', 1),
|
||||||
|
('CUCC', '中国联通', 1),
|
||||||
|
('CTCC', '中国电信', 1);
|
||||||
|
|
||||||
|
-- 1.2 IoT 卡表
|
||||||
|
CREATE TABLE iot_cards (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
iccid VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
card_type VARCHAR(50) NOT NULL,
|
||||||
|
card_category VARCHAR(20) NOT NULL DEFAULT 'normal',
|
||||||
|
carrier_id BIGINT NOT NULL,
|
||||||
|
imsi VARCHAR(50),
|
||||||
|
msisdn VARCHAR(20),
|
||||||
|
batch_no VARCHAR(100),
|
||||||
|
supplier VARCHAR(255),
|
||||||
|
cost_price DECIMAL(10,2) DEFAULT 0,
|
||||||
|
distribute_price DECIMAL(10,2) DEFAULT 0,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
owner_type VARCHAR(20) NOT NULL DEFAULT 'platform',
|
||||||
|
owner_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
activated_at TIMESTAMP,
|
||||||
|
activation_status INT NOT NULL DEFAULT 0,
|
||||||
|
real_name_status INT NOT NULL DEFAULT 0,
|
||||||
|
network_status INT NOT NULL DEFAULT 0,
|
||||||
|
data_usage_mb BIGINT DEFAULT 0,
|
||||||
|
enable_polling BOOLEAN DEFAULT TRUE,
|
||||||
|
last_data_check_at TIMESTAMP,
|
||||||
|
last_real_name_check_at TIMESTAMP,
|
||||||
|
last_sync_time TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE iot_cards IS 'IoT 卡表(物联网卡/流量卡)';
|
||||||
|
COMMENT ON COLUMN iot_cards.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN iot_cards.iccid IS 'ICCID(唯一标识)';
|
||||||
|
COMMENT ON COLUMN iot_cards.card_type IS '卡类型';
|
||||||
|
COMMENT ON COLUMN iot_cards.card_category IS '卡业务类型 normal-普通卡 industry-行业卡';
|
||||||
|
COMMENT ON COLUMN iot_cards.carrier_id IS '运营商ID';
|
||||||
|
COMMENT ON COLUMN iot_cards.imsi IS 'IMSI';
|
||||||
|
COMMENT ON COLUMN iot_cards.msisdn IS 'MSISDN(手机号码)';
|
||||||
|
COMMENT ON COLUMN iot_cards.batch_no IS '批次号';
|
||||||
|
COMMENT ON COLUMN iot_cards.supplier IS '供应商';
|
||||||
|
COMMENT ON COLUMN iot_cards.cost_price IS '成本价(元)';
|
||||||
|
COMMENT ON COLUMN iot_cards.distribute_price IS '分销价(元)';
|
||||||
|
COMMENT ON COLUMN iot_cards.status IS '状态 1-在库 2-已分销 3-已激活 4-已停用';
|
||||||
|
COMMENT ON COLUMN iot_cards.owner_type IS '所有者类型 platform-平台 agent-代理 user-用户 device-设备';
|
||||||
|
COMMENT ON COLUMN iot_cards.owner_id IS '所有者ID';
|
||||||
|
COMMENT ON COLUMN iot_cards.activated_at IS '激活时间';
|
||||||
|
COMMENT ON COLUMN iot_cards.activation_status IS '激活状态 0-未激活 1-已激活';
|
||||||
|
COMMENT ON COLUMN iot_cards.real_name_status IS '实名状态 0-未实名 1-已实名(行业卡可以保持0)';
|
||||||
|
COMMENT ON COLUMN iot_cards.network_status IS '网络状态 0-停机 1-开机';
|
||||||
|
COMMENT ON COLUMN iot_cards.data_usage_mb IS '累计流量使用(MB)';
|
||||||
|
COMMENT ON COLUMN iot_cards.enable_polling IS '是否参与轮询 true-参与 false-不参与';
|
||||||
|
COMMENT ON COLUMN iot_cards.last_data_check_at IS '最后一次流量检查时间';
|
||||||
|
COMMENT ON COLUMN iot_cards.last_real_name_check_at IS '最后一次实名检查时间';
|
||||||
|
COMMENT ON COLUMN iot_cards.last_sync_time IS '最后一次与Gateway同步时间';
|
||||||
|
COMMENT ON COLUMN iot_cards.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN iot_cards.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- IoT 卡表索引
|
||||||
|
CREATE INDEX idx_iot_cards_carrier ON iot_cards(carrier_id);
|
||||||
|
CREATE INDEX idx_iot_cards_owner ON iot_cards(owner_type, owner_id, status);
|
||||||
|
CREATE INDEX idx_iot_cards_batch ON iot_cards(batch_no);
|
||||||
|
CREATE INDEX idx_iot_cards_activated ON iot_cards(activated_at);
|
||||||
|
CREATE INDEX idx_iot_cards_category ON iot_cards(card_category);
|
||||||
|
CREATE INDEX idx_iot_cards_data_check ON iot_cards(enable_polling, activation_status, last_data_check_at);
|
||||||
|
CREATE INDEX idx_iot_cards_real_name_check ON iot_cards(enable_polling, real_name_status, last_real_name_check_at);
|
||||||
|
|
||||||
|
-- 1.3 设备表
|
||||||
|
CREATE TABLE devices (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
device_no VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
device_name VARCHAR(255),
|
||||||
|
device_model VARCHAR(100),
|
||||||
|
device_type VARCHAR(50),
|
||||||
|
max_sim_slots INT DEFAULT 4,
|
||||||
|
manufacturer VARCHAR(255),
|
||||||
|
batch_no VARCHAR(100),
|
||||||
|
owner_type VARCHAR(20) NOT NULL DEFAULT 'platform',
|
||||||
|
owner_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
activated_at TIMESTAMP,
|
||||||
|
device_username VARCHAR(100),
|
||||||
|
device_password_encrypted VARCHAR(255),
|
||||||
|
device_api_endpoint VARCHAR(500),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE devices IS '设备表';
|
||||||
|
COMMENT ON COLUMN devices.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN devices.device_no IS '设备编号(唯一标识)';
|
||||||
|
COMMENT ON COLUMN devices.device_name IS '设备名称';
|
||||||
|
COMMENT ON COLUMN devices.device_model IS '设备型号';
|
||||||
|
COMMENT ON COLUMN devices.device_type IS '设备类型';
|
||||||
|
COMMENT ON COLUMN devices.max_sim_slots IS '最大插槽数量(默认4)';
|
||||||
|
COMMENT ON COLUMN devices.manufacturer IS '制造商';
|
||||||
|
COMMENT ON COLUMN devices.batch_no IS '批次号';
|
||||||
|
COMMENT ON COLUMN devices.owner_type IS '所有者类型 platform-平台 agent-代理 user-用户';
|
||||||
|
COMMENT ON COLUMN devices.owner_id IS '所有者ID';
|
||||||
|
COMMENT ON COLUMN devices.status IS '状态 1-在库 2-已分销 3-已激活 4-已停用';
|
||||||
|
COMMENT ON COLUMN devices.activated_at IS '激活时间';
|
||||||
|
COMMENT ON COLUMN devices.device_username IS '设备登录用户名';
|
||||||
|
COMMENT ON COLUMN devices.device_password_encrypted IS '设备登录密码(加密)';
|
||||||
|
COMMENT ON COLUMN devices.device_api_endpoint IS '设备API端点';
|
||||||
|
COMMENT ON COLUMN devices.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN devices.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 设备表索引
|
||||||
|
CREATE INDEX idx_devices_owner ON devices(owner_type, owner_id, status);
|
||||||
|
|
||||||
|
-- 1.4 号卡表
|
||||||
|
CREATE TABLE number_cards (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
virtual_product_code VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
card_name VARCHAR(255) NOT NULL,
|
||||||
|
card_type VARCHAR(50),
|
||||||
|
carrier VARCHAR(50),
|
||||||
|
data_amount_mb BIGINT,
|
||||||
|
price DECIMAL(10,2),
|
||||||
|
agent_id BIGINT,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE number_cards IS '号卡表';
|
||||||
|
COMMENT ON COLUMN number_cards.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN number_cards.virtual_product_code IS '虚拟商品编码(用于对应运营商订单)';
|
||||||
|
COMMENT ON COLUMN number_cards.card_name IS '号卡名称';
|
||||||
|
COMMENT ON COLUMN number_cards.card_type IS '号卡类型';
|
||||||
|
COMMENT ON COLUMN number_cards.carrier IS '运营商';
|
||||||
|
COMMENT ON COLUMN number_cards.data_amount_mb IS '流量额度(MB)';
|
||||||
|
COMMENT ON COLUMN number_cards.price IS '价格(元)';
|
||||||
|
COMMENT ON COLUMN number_cards.agent_id IS '代理用户ID';
|
||||||
|
COMMENT ON COLUMN number_cards.status IS '状态 1-在售 2-下架';
|
||||||
|
COMMENT ON COLUMN number_cards.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN number_cards.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 号卡表索引
|
||||||
|
CREATE INDEX idx_number_cards_agent ON number_cards(agent_id, status);
|
||||||
|
|
||||||
|
-- 1.5 套餐系列表
|
||||||
|
CREATE TABLE package_series (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
series_code VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
series_name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE package_series IS '套餐系列表';
|
||||||
|
COMMENT ON COLUMN package_series.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN package_series.series_code IS '系列编码';
|
||||||
|
COMMENT ON COLUMN package_series.series_name IS '系列名称';
|
||||||
|
COMMENT ON COLUMN package_series.description IS '描述';
|
||||||
|
COMMENT ON COLUMN package_series.status IS '状态 1-启用 2-禁用';
|
||||||
|
COMMENT ON COLUMN package_series.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN package_series.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 1.6 套餐表
|
||||||
|
CREATE TABLE packages (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
package_code VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
package_name VARCHAR(255) NOT NULL,
|
||||||
|
series_id BIGINT,
|
||||||
|
package_type VARCHAR(50) NOT NULL,
|
||||||
|
duration_months INT NOT NULL,
|
||||||
|
data_type VARCHAR(20),
|
||||||
|
real_data_mb BIGINT DEFAULT 0,
|
||||||
|
virtual_data_mb BIGINT DEFAULT 0,
|
||||||
|
data_amount_mb BIGINT DEFAULT 0,
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE packages IS '套餐表';
|
||||||
|
COMMENT ON COLUMN packages.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN packages.package_code IS '套餐编码';
|
||||||
|
COMMENT ON COLUMN packages.package_name IS '套餐名称';
|
||||||
|
COMMENT ON COLUMN packages.series_id IS '套餐系列ID';
|
||||||
|
COMMENT ON COLUMN packages.package_type IS '套餐类型 formal-正式套餐 addon-附加套餐';
|
||||||
|
COMMENT ON COLUMN packages.duration_months IS '套餐时长(月数) 1-月套餐 12-年套餐';
|
||||||
|
COMMENT ON COLUMN packages.data_type IS '流量类型 real-真流量 virtual-虚流量';
|
||||||
|
COMMENT ON COLUMN packages.real_data_mb IS '真流量额度(MB)';
|
||||||
|
COMMENT ON COLUMN packages.virtual_data_mb IS '虚流量额度(MB,用于停机判断)';
|
||||||
|
COMMENT ON COLUMN packages.data_amount_mb IS '总流量额度(MB)';
|
||||||
|
COMMENT ON COLUMN packages.price IS '套餐价格(元)';
|
||||||
|
COMMENT ON COLUMN packages.status IS '状态 1-启用 2-禁用';
|
||||||
|
COMMENT ON COLUMN packages.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN packages.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 套餐表索引
|
||||||
|
CREATE INDEX idx_packages_series ON packages(series_id, status);
|
||||||
|
|
||||||
|
-- 1.7 代理套餐分配表
|
||||||
|
CREATE TABLE agent_package_allocations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
package_id BIGINT NOT NULL,
|
||||||
|
cost_price DECIMAL(10,2) NOT NULL,
|
||||||
|
retail_price DECIMAL(10,2) NOT NULL,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
CONSTRAINT uk_agent_package UNIQUE (agent_id, package_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE agent_package_allocations IS '代理套餐分配表';
|
||||||
|
COMMENT ON COLUMN agent_package_allocations.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN agent_package_allocations.agent_id IS '代理用户ID';
|
||||||
|
COMMENT ON COLUMN agent_package_allocations.package_id IS '套餐ID';
|
||||||
|
COMMENT ON COLUMN agent_package_allocations.cost_price IS '成本价(元)';
|
||||||
|
COMMENT ON COLUMN agent_package_allocations.retail_price IS '零售价(元)';
|
||||||
|
COMMENT ON COLUMN agent_package_allocations.status IS '状态 1-启用 2-禁用';
|
||||||
|
COMMENT ON COLUMN agent_package_allocations.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN agent_package_allocations.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 1.8 设备-IoT卡绑定关系表
|
||||||
|
CREATE TABLE device_sim_bindings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
device_id BIGINT NOT NULL,
|
||||||
|
iot_card_id BIGINT NOT NULL,
|
||||||
|
slot_position INT,
|
||||||
|
bind_status INT DEFAULT 1,
|
||||||
|
bind_time TIMESTAMP,
|
||||||
|
unbind_time TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE device_sim_bindings IS '设备-IoT卡绑定关系表';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.device_id IS '设备ID';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.iot_card_id IS 'IoT卡ID';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.slot_position IS '插槽位置(1, 2, 3, 4)';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.bind_status IS '绑定状态 1-已绑定 2-已解绑';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.bind_time IS '绑定时间';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.unbind_time IS '解绑时间';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN device_sim_bindings.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 设备-IoT卡绑定关系表索引
|
||||||
|
CREATE INDEX idx_device_sim_bindings_device ON device_sim_bindings(device_id, bind_status);
|
||||||
|
CREATE INDEX idx_device_sim_bindings_iot_card ON device_sim_bindings(iot_card_id, bind_status);
|
||||||
|
CREATE UNIQUE INDEX idx_device_sim_bindings_active_card ON device_sim_bindings(iot_card_id) WHERE bind_status = 1;
|
||||||
|
|
||||||
|
-- 1.9 订单表
|
||||||
|
CREATE TABLE orders (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
order_no VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
order_type INT NOT NULL,
|
||||||
|
iot_card_id BIGINT,
|
||||||
|
device_id BIGINT,
|
||||||
|
number_card_id BIGINT,
|
||||||
|
package_id BIGINT,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
agent_id BIGINT,
|
||||||
|
amount DECIMAL(10,2) NOT NULL,
|
||||||
|
payment_method VARCHAR(20),
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
carrier_order_id VARCHAR(255),
|
||||||
|
carrier_order_data JSONB,
|
||||||
|
paid_at TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE orders IS '订单表';
|
||||||
|
COMMENT ON COLUMN orders.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN orders.order_no IS '订单号(唯一标识)';
|
||||||
|
COMMENT ON COLUMN orders.order_type IS '订单类型 1-套餐订单 2-号卡订单';
|
||||||
|
COMMENT ON COLUMN orders.iot_card_id IS 'IoT卡ID(单卡套餐订单时有值)';
|
||||||
|
COMMENT ON COLUMN orders.device_id IS '设备ID(设备级套餐订单时有值)';
|
||||||
|
COMMENT ON COLUMN orders.number_card_id IS '号卡ID(号卡订单时有值)';
|
||||||
|
COMMENT ON COLUMN orders.package_id IS '套餐ID(套餐订单时有值)';
|
||||||
|
COMMENT ON COLUMN orders.user_id IS '用户ID';
|
||||||
|
COMMENT ON COLUMN orders.agent_id IS '代理用户ID';
|
||||||
|
COMMENT ON COLUMN orders.amount IS '订单金额(元)';
|
||||||
|
COMMENT ON COLUMN orders.payment_method IS '支付方式 wallet-钱包 online-在线支付 carrier-运营商支付';
|
||||||
|
COMMENT ON COLUMN orders.status IS '状态 1-待支付 2-已支付 3-已完成 4-已取消 5-已退款';
|
||||||
|
COMMENT ON COLUMN orders.carrier_order_id IS '运营商订单ID';
|
||||||
|
COMMENT ON COLUMN orders.carrier_order_data IS '运营商订单原始数据(JSON)';
|
||||||
|
COMMENT ON COLUMN orders.paid_at IS '支付时间';
|
||||||
|
COMMENT ON COLUMN orders.completed_at IS '完成时间';
|
||||||
|
COMMENT ON COLUMN orders.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN orders.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 订单表索引
|
||||||
|
CREATE INDEX idx_orders_user ON orders(user_id, status);
|
||||||
|
CREATE INDEX idx_orders_agent ON orders(agent_id, status);
|
||||||
|
CREATE INDEX idx_orders_iot_card ON orders(iot_card_id);
|
||||||
|
CREATE INDEX idx_orders_device ON orders(device_id);
|
||||||
|
CREATE INDEX idx_orders_number_card ON orders(number_card_id);
|
||||||
|
|
||||||
|
-- ========================================
|
||||||
|
-- 2. 套餐和轮询相关表
|
||||||
|
-- ========================================
|
||||||
|
|
||||||
|
-- 2.1 套餐使用情况表
|
||||||
|
CREATE TABLE package_usages (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
order_id BIGINT NOT NULL,
|
||||||
|
package_id BIGINT NOT NULL,
|
||||||
|
usage_type VARCHAR(20) NOT NULL,
|
||||||
|
iot_card_id BIGINT,
|
||||||
|
device_id BIGINT,
|
||||||
|
data_limit_mb BIGINT NOT NULL,
|
||||||
|
data_usage_mb BIGINT DEFAULT 0,
|
||||||
|
real_data_usage_mb BIGINT DEFAULT 0,
|
||||||
|
virtual_data_usage_mb BIGINT DEFAULT 0,
|
||||||
|
activated_at TIMESTAMP NOT NULL,
|
||||||
|
expires_at TIMESTAMP NOT NULL,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
last_package_check_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE package_usages IS '套餐使用情况表';
|
||||||
|
COMMENT ON COLUMN package_usages.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN package_usages.order_id IS '订单ID';
|
||||||
|
COMMENT ON COLUMN package_usages.package_id IS '套餐ID';
|
||||||
|
COMMENT ON COLUMN package_usages.usage_type IS '使用类型 single_card-单卡套餐 device-设备级套餐';
|
||||||
|
COMMENT ON COLUMN package_usages.iot_card_id IS 'IoT卡ID(单卡套餐时有值)';
|
||||||
|
COMMENT ON COLUMN package_usages.device_id IS '设备ID(设备级套餐时有值)';
|
||||||
|
COMMENT ON COLUMN package_usages.data_limit_mb IS '流量限额(MB)';
|
||||||
|
COMMENT ON COLUMN package_usages.data_usage_mb IS '已使用流量(MB)';
|
||||||
|
COMMENT ON COLUMN package_usages.real_data_usage_mb IS '真流量使用(MB)';
|
||||||
|
COMMENT ON COLUMN package_usages.virtual_data_usage_mb IS '虚流量使用(MB)';
|
||||||
|
COMMENT ON COLUMN package_usages.activated_at IS '套餐生效时间';
|
||||||
|
COMMENT ON COLUMN package_usages.expires_at IS '套餐过期时间';
|
||||||
|
COMMENT ON COLUMN package_usages.status IS '状态 1-生效中 2-已用完 3-已过期';
|
||||||
|
COMMENT ON COLUMN package_usages.last_package_check_at IS '最后一次套餐流量检查时间';
|
||||||
|
COMMENT ON COLUMN package_usages.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN package_usages.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 套餐使用情况表索引
|
||||||
|
CREATE INDEX idx_package_usages_order ON package_usages(order_id);
|
||||||
|
CREATE INDEX idx_package_usages_package ON package_usages(package_id);
|
||||||
|
CREATE INDEX idx_package_usages_iot_card ON package_usages(iot_card_id);
|
||||||
|
CREATE INDEX idx_package_usages_device ON package_usages(device_id);
|
||||||
|
CREATE INDEX idx_package_usages_check ON package_usages(status, expires_at, last_package_check_at);
|
||||||
|
|
||||||
|
-- 2.2 轮询配置表
|
||||||
|
CREATE TABLE polling_configs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
config_name VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
description VARCHAR(500),
|
||||||
|
card_condition VARCHAR(50),
|
||||||
|
carrier_id BIGINT,
|
||||||
|
real_name_check_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
real_name_check_interval INT DEFAULT 60,
|
||||||
|
card_data_check_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
card_data_check_interval INT DEFAULT 60,
|
||||||
|
package_check_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
package_check_interval INT DEFAULT 60,
|
||||||
|
priority INT NOT NULL DEFAULT 100,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE polling_configs IS '轮询配置表';
|
||||||
|
COMMENT ON COLUMN polling_configs.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN polling_configs.config_name IS '配置名称(如 未实名卡、实名卡)';
|
||||||
|
COMMENT ON COLUMN polling_configs.description IS '配置描述';
|
||||||
|
COMMENT ON COLUMN polling_configs.card_condition IS '卡状态条件 not_real_name-未实名 real_name-已实名 activated-已激活 suspended-已停用';
|
||||||
|
COMMENT ON COLUMN polling_configs.carrier_id IS '运营商ID(NULL表示所有运营商)';
|
||||||
|
COMMENT ON COLUMN polling_configs.real_name_check_enabled IS '是否启用实名检查';
|
||||||
|
COMMENT ON COLUMN polling_configs.real_name_check_interval IS '实名检查间隔(秒)';
|
||||||
|
COMMENT ON COLUMN polling_configs.card_data_check_enabled IS '是否启用卡流量检查';
|
||||||
|
COMMENT ON COLUMN polling_configs.card_data_check_interval IS '卡流量检查间隔(秒)';
|
||||||
|
COMMENT ON COLUMN polling_configs.package_check_enabled IS '是否启用套餐流量检查';
|
||||||
|
COMMENT ON COLUMN polling_configs.package_check_interval IS '套餐流量检查间隔(秒)';
|
||||||
|
COMMENT ON COLUMN polling_configs.priority IS '优先级(数字越小优先级越高)';
|
||||||
|
COMMENT ON COLUMN polling_configs.status IS '状态 1-启用 2-禁用';
|
||||||
|
COMMENT ON COLUMN polling_configs.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN polling_configs.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 轮询配置表索引
|
||||||
|
CREATE INDEX idx_polling_configs_match ON polling_configs(status, card_condition, carrier_id, priority);
|
||||||
|
|
||||||
|
-- 2.3 流量使用记录表
|
||||||
|
CREATE TABLE data_usage_records (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
iot_card_id BIGINT NOT NULL,
|
||||||
|
data_usage_mb BIGINT NOT NULL,
|
||||||
|
data_increase_mb BIGINT DEFAULT 0,
|
||||||
|
check_time TIMESTAMP NOT NULL,
|
||||||
|
source VARCHAR(50) DEFAULT 'polling',
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE data_usage_records IS '流量使用记录表';
|
||||||
|
COMMENT ON COLUMN data_usage_records.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN data_usage_records.iot_card_id IS 'IoT卡ID';
|
||||||
|
COMMENT ON COLUMN data_usage_records.data_usage_mb IS '流量使用量(MB)';
|
||||||
|
COMMENT ON COLUMN data_usage_records.data_increase_mb IS '相比上次的增量(MB)';
|
||||||
|
COMMENT ON COLUMN data_usage_records.check_time IS '检查时间';
|
||||||
|
COMMENT ON COLUMN data_usage_records.source IS '数据来源 polling-轮询 manual-手动 gateway-回调';
|
||||||
|
COMMENT ON COLUMN data_usage_records.created_at IS '创建时间';
|
||||||
|
|
||||||
|
-- 流量使用记录表索引
|
||||||
|
CREATE INDEX idx_data_usage_records_card_time ON data_usage_records(iot_card_id, check_time DESC);
|
||||||
|
CREATE INDEX idx_data_usage_records_time ON data_usage_records(check_time);
|
||||||
|
|
||||||
|
-- ========================================
|
||||||
|
-- 3. 分佣相关表
|
||||||
|
-- ========================================
|
||||||
|
|
||||||
|
-- 3.1 代理层级关系表
|
||||||
|
CREATE TABLE agent_hierarchies (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL UNIQUE,
|
||||||
|
parent_agent_id BIGINT,
|
||||||
|
level INT NOT NULL,
|
||||||
|
path VARCHAR(500),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE agent_hierarchies IS '代理层级关系表';
|
||||||
|
COMMENT ON COLUMN agent_hierarchies.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN agent_hierarchies.agent_id IS '代理用户ID';
|
||||||
|
COMMENT ON COLUMN agent_hierarchies.parent_agent_id IS '上级代理用户ID(NULL表示顶级代理)';
|
||||||
|
COMMENT ON COLUMN agent_hierarchies.level IS '代理层级(1, 2, 3...)';
|
||||||
|
COMMENT ON COLUMN agent_hierarchies.path IS '代理路径(如: "1/5/12")';
|
||||||
|
COMMENT ON COLUMN agent_hierarchies.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN agent_hierarchies.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 代理层级关系表索引
|
||||||
|
CREATE INDEX idx_agent_hierarchies_parent ON agent_hierarchies(parent_agent_id);
|
||||||
|
|
||||||
|
-- 3.2 分佣规则表
|
||||||
|
CREATE TABLE commission_rules (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
business_type VARCHAR(50) NOT NULL,
|
||||||
|
card_type VARCHAR(50) NOT NULL,
|
||||||
|
series_id BIGINT,
|
||||||
|
package_id BIGINT,
|
||||||
|
commission_type VARCHAR(50) NOT NULL,
|
||||||
|
commission_mode VARCHAR(20) NOT NULL,
|
||||||
|
commission_value DECIMAL(10,2) NOT NULL,
|
||||||
|
unfreeze_days INT DEFAULT 0,
|
||||||
|
min_activation_for_unfreeze INT DEFAULT 0,
|
||||||
|
approval_type VARCHAR(20) DEFAULT 'auto',
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE commission_rules IS '分佣规则表';
|
||||||
|
COMMENT ON COLUMN commission_rules.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN commission_rules.agent_id IS '代理用户ID';
|
||||||
|
COMMENT ON COLUMN commission_rules.business_type IS '业务类型';
|
||||||
|
COMMENT ON COLUMN commission_rules.card_type IS '卡类型 number_card-号卡 iot_card-IoT卡';
|
||||||
|
COMMENT ON COLUMN commission_rules.series_id IS '套餐系列ID(一次性分佣时用)';
|
||||||
|
COMMENT ON COLUMN commission_rules.package_id IS '套餐ID(长期分佣时用)';
|
||||||
|
COMMENT ON COLUMN commission_rules.commission_type IS '分佣类型 one_time-一次性 long_term-长期 combined-组合';
|
||||||
|
COMMENT ON COLUMN commission_rules.commission_mode IS '分佣模式 fixed-固定金额 percent-百分比';
|
||||||
|
COMMENT ON COLUMN commission_rules.commission_value IS '分佣值';
|
||||||
|
COMMENT ON COLUMN commission_rules.unfreeze_days IS '解冻天数';
|
||||||
|
COMMENT ON COLUMN commission_rules.min_activation_for_unfreeze IS '解冻最小激活量';
|
||||||
|
COMMENT ON COLUMN commission_rules.approval_type IS '审批类型 auto-自动 manual-人工';
|
||||||
|
COMMENT ON COLUMN commission_rules.status IS '状态 1-启用 2-禁用';
|
||||||
|
COMMENT ON COLUMN commission_rules.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN commission_rules.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 分佣规则表索引
|
||||||
|
CREATE INDEX idx_commission_rules_agent ON commission_rules(agent_id, business_type, card_type);
|
||||||
|
|
||||||
|
-- 3.3 阶梯分佣配置表
|
||||||
|
CREATE TABLE commission_ladder (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
rule_id BIGINT NOT NULL,
|
||||||
|
ladder_type VARCHAR(50) NOT NULL,
|
||||||
|
threshold_value INT NOT NULL,
|
||||||
|
commission_mode VARCHAR(20) NOT NULL,
|
||||||
|
commission_value DECIMAL(10,2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE commission_ladder IS '阶梯分佣配置表';
|
||||||
|
COMMENT ON COLUMN commission_ladder.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN commission_ladder.rule_id IS '分佣规则ID';
|
||||||
|
COMMENT ON COLUMN commission_ladder.ladder_type IS '阶梯类型 activation-激活量 pickup-提货量 deposit-充值量';
|
||||||
|
COMMENT ON COLUMN commission_ladder.threshold_value IS '阈值';
|
||||||
|
COMMENT ON COLUMN commission_ladder.commission_mode IS '分佣模式 fixed-固定金额 percent-百分比';
|
||||||
|
COMMENT ON COLUMN commission_ladder.commission_value IS '分佣值';
|
||||||
|
COMMENT ON COLUMN commission_ladder.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN commission_ladder.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 阶梯分佣配置表索引
|
||||||
|
CREATE INDEX idx_commission_ladder_rule ON commission_ladder(rule_id);
|
||||||
|
|
||||||
|
-- 3.4 组合分佣条件表
|
||||||
|
CREATE TABLE commission_combined_conditions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
rule_id BIGINT NOT NULL UNIQUE,
|
||||||
|
one_time_commission_mode VARCHAR(20),
|
||||||
|
one_time_commission_value DECIMAL(10,2),
|
||||||
|
long_term_commission_mode VARCHAR(20),
|
||||||
|
long_term_commission_value DECIMAL(10,2),
|
||||||
|
long_term_trigger_time_point TIMESTAMP,
|
||||||
|
long_term_trigger_package_cycles INT,
|
||||||
|
long_term_trigger_network_months INT,
|
||||||
|
long_term_unfreeze_days INT DEFAULT 0,
|
||||||
|
long_term_min_activation INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE commission_combined_conditions IS '组合分佣条件表';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.rule_id IS '分佣规则ID';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.one_time_commission_mode IS '一次性分佣模式 fixed-固定金额 percent-百分比';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.one_time_commission_value IS '一次性分佣值';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.long_term_commission_mode IS '长期分佣模式 fixed-固定金额 percent-百分比';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.long_term_commission_value IS '长期分佣值';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.long_term_trigger_time_point IS '长期分佣触发时间点(如实名后3个月)';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.long_term_trigger_package_cycles IS '长期分佣触发套餐周期数(如10个套餐周期)';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.long_term_trigger_network_months IS '长期分佣触发在网月数(号卡专用)';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.long_term_unfreeze_days IS '长期分佣解冻天数';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.long_term_min_activation IS '长期分佣解冻最小激活量';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN commission_combined_conditions.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 3.5 分佣记录表
|
||||||
|
CREATE TABLE commission_records (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
order_id BIGINT NOT NULL,
|
||||||
|
rule_id BIGINT NOT NULL,
|
||||||
|
commission_type VARCHAR(50) NOT NULL,
|
||||||
|
amount DECIMAL(10,2) NOT NULL,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
unfrozen_at TIMESTAMP,
|
||||||
|
released_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE commission_records IS '分佣记录表';
|
||||||
|
COMMENT ON COLUMN commission_records.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN commission_records.agent_id IS '代理用户ID';
|
||||||
|
COMMENT ON COLUMN commission_records.order_id IS '订单ID';
|
||||||
|
COMMENT ON COLUMN commission_records.rule_id IS '分佣规则ID';
|
||||||
|
COMMENT ON COLUMN commission_records.commission_type IS '分佣类型 one_time-一次性 long_term-长期';
|
||||||
|
COMMENT ON COLUMN commission_records.amount IS '分佣金额(元)';
|
||||||
|
COMMENT ON COLUMN commission_records.status IS '状态 1-已冻结 2-解冻中 3-已发放 4-已失效';
|
||||||
|
COMMENT ON COLUMN commission_records.unfrozen_at IS '解冻时间';
|
||||||
|
COMMENT ON COLUMN commission_records.released_at IS '发放时间';
|
||||||
|
COMMENT ON COLUMN commission_records.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN commission_records.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 分佣记录表索引
|
||||||
|
CREATE INDEX idx_commission_records_agent ON commission_records(agent_id, status);
|
||||||
|
CREATE INDEX idx_commission_records_order ON commission_records(order_id);
|
||||||
|
CREATE INDEX idx_commission_records_rule ON commission_records(rule_id);
|
||||||
|
|
||||||
|
-- 3.6 分佣审批表
|
||||||
|
CREATE TABLE commission_approvals (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
commission_record_id BIGINT NOT NULL,
|
||||||
|
approver_id BIGINT,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
reason TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE commission_approvals IS '分佣审批表';
|
||||||
|
COMMENT ON COLUMN commission_approvals.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN commission_approvals.commission_record_id IS '分佣记录ID';
|
||||||
|
COMMENT ON COLUMN commission_approvals.approver_id IS '审批人用户ID';
|
||||||
|
COMMENT ON COLUMN commission_approvals.status IS '状态 1-待审批 2-已通过 3-已拒绝';
|
||||||
|
COMMENT ON COLUMN commission_approvals.reason IS '原因';
|
||||||
|
COMMENT ON COLUMN commission_approvals.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN commission_approvals.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 分佣审批表索引
|
||||||
|
CREATE INDEX idx_commission_approvals_record ON commission_approvals(commission_record_id);
|
||||||
|
CREATE INDEX idx_commission_approvals_status ON commission_approvals(status);
|
||||||
|
|
||||||
|
-- 3.7 分佣模板表
|
||||||
|
CREATE TABLE commission_templates (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
template_name VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
business_type VARCHAR(50) NOT NULL,
|
||||||
|
card_type VARCHAR(50) NOT NULL,
|
||||||
|
commission_type VARCHAR(50) NOT NULL,
|
||||||
|
commission_mode VARCHAR(20) NOT NULL,
|
||||||
|
commission_value DECIMAL(10,2) NOT NULL,
|
||||||
|
unfreeze_days INT DEFAULT 0,
|
||||||
|
min_activation_for_unfreeze INT DEFAULT 0,
|
||||||
|
approval_type VARCHAR(20) DEFAULT 'auto',
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE commission_templates IS '分佣模板表';
|
||||||
|
COMMENT ON COLUMN commission_templates.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN commission_templates.template_name IS '模板名称';
|
||||||
|
COMMENT ON COLUMN commission_templates.business_type IS '业务类型';
|
||||||
|
COMMENT ON COLUMN commission_templates.card_type IS '卡类型 number_card-号卡 iot_card-IoT卡';
|
||||||
|
COMMENT ON COLUMN commission_templates.commission_type IS '分佣类型 one_time-一次性 long_term-长期 combined-组合';
|
||||||
|
COMMENT ON COLUMN commission_templates.commission_mode IS '分佣模式 fixed-固定金额 percent-百分比';
|
||||||
|
COMMENT ON COLUMN commission_templates.commission_value IS '分佣值';
|
||||||
|
COMMENT ON COLUMN commission_templates.unfreeze_days IS '解冻天数';
|
||||||
|
COMMENT ON COLUMN commission_templates.min_activation_for_unfreeze IS '解冻最小激活量';
|
||||||
|
COMMENT ON COLUMN commission_templates.approval_type IS '审批类型 auto-自动 manual-人工';
|
||||||
|
COMMENT ON COLUMN commission_templates.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN commission_templates.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 3.8 号卡运营商结算表
|
||||||
|
CREATE TABLE carrier_settlements (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
commission_record_id BIGINT NOT NULL UNIQUE,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
settlement_month VARCHAR(20) NOT NULL,
|
||||||
|
settlement_amount DECIMAL(18,2) NOT NULL,
|
||||||
|
status INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE carrier_settlements IS '号卡运营商结算表';
|
||||||
|
COMMENT ON COLUMN carrier_settlements.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN carrier_settlements.commission_record_id IS '分佣记录ID';
|
||||||
|
COMMENT ON COLUMN carrier_settlements.agent_id IS '代理用户ID';
|
||||||
|
COMMENT ON COLUMN carrier_settlements.settlement_month IS '结算月份(如 2026-01)';
|
||||||
|
COMMENT ON COLUMN carrier_settlements.settlement_amount IS '结算金额(元)';
|
||||||
|
COMMENT ON COLUMN carrier_settlements.status IS '状态 1-待结算 2-已结算';
|
||||||
|
COMMENT ON COLUMN carrier_settlements.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN carrier_settlements.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 号卡运营商结算表索引
|
||||||
|
CREATE INDEX idx_carrier_settlements_agent ON carrier_settlements(agent_id, status);
|
||||||
|
|
||||||
|
-- ========================================
|
||||||
|
-- 4. 财务管理表
|
||||||
|
-- ========================================
|
||||||
|
|
||||||
|
-- 4.1 佣金提现申请表
|
||||||
|
CREATE TABLE commission_withdrawal_requests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
amount DECIMAL(18,2) NOT NULL,
|
||||||
|
fee DECIMAL(18,2) DEFAULT 0,
|
||||||
|
actual_amount DECIMAL(18,2),
|
||||||
|
withdrawal_method VARCHAR(20),
|
||||||
|
account_info JSONB,
|
||||||
|
status INT DEFAULT 1,
|
||||||
|
approved_by BIGINT,
|
||||||
|
approved_at TIMESTAMP,
|
||||||
|
paid_at TIMESTAMP,
|
||||||
|
reject_reason TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE commission_withdrawal_requests IS '佣金提现申请表';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.agent_id IS '代理用户ID';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.amount IS '提现金额(元)';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.fee IS '手续费(元)';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.actual_amount IS '实际到账金额(元)';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.withdrawal_method IS '提现方式 alipay-支付宝 wechat-微信 bank-银行卡';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.account_info IS '收款账户信息(姓名、账号等)';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.status IS '状态 1-待审核 2-已通过 3-已拒绝 4-已到账';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.approved_by IS '审批人用户ID';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.approved_at IS '审批时间';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.paid_at IS '到账时间';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.reject_reason IS '拒绝原因';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_requests.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 佣金提现申请表索引
|
||||||
|
CREATE INDEX idx_commission_withdrawal_requests_agent ON commission_withdrawal_requests(agent_id, status);
|
||||||
|
CREATE INDEX idx_commission_withdrawal_requests_created ON commission_withdrawal_requests(created_at);
|
||||||
|
|
||||||
|
-- 4.2 佣金提现设置表
|
||||||
|
CREATE TABLE commission_withdrawal_settings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
min_withdrawal_amount DECIMAL(10,2),
|
||||||
|
fee_rate DECIMAL(5,4),
|
||||||
|
arrival_days INT,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE commission_withdrawal_settings IS '佣金提现设置表';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_settings.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_settings.min_withdrawal_amount IS '最低提现金额(元)';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_settings.fee_rate IS '手续费率(如 0.01 表示 1%)';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_settings.arrival_days IS '到账天数';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_settings.is_active IS '是否生效(最新一条)';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_settings.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN commission_withdrawal_settings.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 4.3 收款商户设置表
|
||||||
|
CREATE TABLE payment_merchant_settings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
merchant_type VARCHAR(20),
|
||||||
|
account_name VARCHAR(255),
|
||||||
|
account_number VARCHAR(255),
|
||||||
|
bank_name VARCHAR(255),
|
||||||
|
bank_branch VARCHAR(255),
|
||||||
|
is_verified BOOLEAN DEFAULT FALSE,
|
||||||
|
is_default BOOLEAN DEFAULT FALSE,
|
||||||
|
status INT DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE payment_merchant_settings IS '收款商户设置表';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.user_id IS '用户ID';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.merchant_type IS '商户类型 alipay-支付宝 wechat-微信 bank-银行卡';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.account_name IS '账户名称';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.account_number IS '账号';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.bank_name IS '银行名称(仅银行卡)';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.bank_branch IS '开户行(仅银行卡)';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.is_verified IS '是否已验证';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.is_default IS '是否默认账户';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.status IS '状态 1-启用 2-禁用';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN payment_merchant_settings.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 收款商户设置表索引
|
||||||
|
CREATE INDEX idx_payment_merchant_settings_user ON payment_merchant_settings(user_id, is_default);
|
||||||
|
CREATE INDEX idx_payment_merchant_settings_type ON payment_merchant_settings(merchant_type, status);
|
||||||
|
|
||||||
|
-- ========================================
|
||||||
|
-- 5. 系统管理表
|
||||||
|
-- ========================================
|
||||||
|
|
||||||
|
-- 5.1 开发能力配置表
|
||||||
|
CREATE TABLE dev_capability_configs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
app_name VARCHAR(255),
|
||||||
|
app_id VARCHAR(100) UNIQUE,
|
||||||
|
app_secret VARCHAR(255),
|
||||||
|
callback_url VARCHAR(500),
|
||||||
|
ip_whitelist TEXT,
|
||||||
|
status INT DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE dev_capability_configs IS '开发能力配置表';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.user_id IS '用户ID(平台或代理)';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.app_name IS '应用名称';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.app_id IS '应用ID';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.app_secret IS '应用密钥';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.callback_url IS '回调地址';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.ip_whitelist IS 'IP白名单(多个IP用逗号分隔)';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.status IS '状态 1-启用 2-禁用';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN dev_capability_configs.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 开发能力配置表索引
|
||||||
|
CREATE INDEX idx_dev_capability_configs_user ON dev_capability_configs(user_id, status);
|
||||||
|
|
||||||
|
-- 5.2 换卡申请表
|
||||||
|
CREATE TABLE card_replacement_requests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
old_iccid VARCHAR(50) NOT NULL,
|
||||||
|
new_iccid VARCHAR(50),
|
||||||
|
reason TEXT,
|
||||||
|
status INT DEFAULT 1,
|
||||||
|
approved_by BIGINT,
|
||||||
|
approved_at TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP,
|
||||||
|
reject_reason TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE card_replacement_requests IS '换卡申请表';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.user_id IS '申请用户ID';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.old_iccid IS '旧卡ICCID';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.new_iccid IS '新卡ICCID(审批时填充)';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.reason IS '换卡原因';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.status IS '状态 1-待处理 2-已通过 3-已拒绝 4-已完成';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.approved_by IS '处理人用户ID';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.approved_at IS '处理时间';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.completed_at IS '完成时间(新卡激活时间)';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.reject_reason IS '拒绝原因';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN card_replacement_requests.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 换卡申请表索引
|
||||||
|
CREATE INDEX idx_card_replacement_requests_user ON card_replacement_requests(user_id, status);
|
||||||
|
CREATE INDEX idx_card_replacement_requests_old_iccid ON card_replacement_requests(old_iccid);
|
||||||
|
CREATE INDEX idx_card_replacement_requests_new_iccid ON card_replacement_requests(new_iccid);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-01-10
|
||||||
964
openspec/changes/archive/2026-01-12-iot-sim-management/design.md
Normal file
964
openspec/changes/archive/2026-01-12-iot-sim-management/design.md
Normal file
@@ -0,0 +1,964 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
|
||||||
|
junhong_cmp_fiber 项目需要构建 IoT 卡管理系统,支持三大核心业务:
|
||||||
|
|
||||||
|
**核心概念澄清**:
|
||||||
|
- **IoT 卡** = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)
|
||||||
|
- **普通卡**: 需要实名认证才能激活使用,遵循运营商实名制要求
|
||||||
|
- **行业卡**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景
|
||||||
|
- **设备**: 用户的物联网设备(如 GPS 追踪器、智能传感器),可绑定 1-4 张 IoT 卡,主要用于批量管理和设备操作(重启、修改密码等),不在卡管系统中销售
|
||||||
|
- **号卡**: 完全独立的业务线,从上游平台下单,不走我们平台激活和充值,只接收订单状态更新
|
||||||
|
|
||||||
|
**三大核心业务**:
|
||||||
|
1. **IoT 卡(IotCard)**: 平台自营销售和代理分销,通过购买套餐产生订单,使用 ICCID 作为唯一标识
|
||||||
|
2. **设备(Device)**: 用户设备管理,可绑定 1-4 张 IoT 卡,支持设备级套餐购买(流量共享),不在卡管系统中销售
|
||||||
|
3. **号卡(NumberCard)**: 运营商订单回传,使用虚拟商品编码映射,支持代理分销和分佣
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
|
||||||
|
- 已有用户体系:平台用户、代理用户、企业用户、个人用户(`user_organizations`, `users` 等表)
|
||||||
|
- 已有认证和权限系统(`auth`, `role-permission`, `data-permission`)
|
||||||
|
- 外部依赖:Gateway 项目提供 IoT 卡状态、实名、流量、停复机等 HTTP 接口
|
||||||
|
|
||||||
|
### 约束
|
||||||
|
|
||||||
|
- 本阶段只设计数据模型层(域实体、ERD、表结构、Schema、GORM Models)
|
||||||
|
- 不涉及 API/Handler/Service 层的实现
|
||||||
|
- 不涉及计费系统、供应管理、事件系统的实现
|
||||||
|
- 遵循项目规范:无外键约束、无 ORM 关联、手动维护关联关系
|
||||||
|
|
||||||
|
### 利益相关方
|
||||||
|
|
||||||
|
- 平台用户:自营销售 IoT 卡、管理设备
|
||||||
|
- 代理商:多级树形结构,分销 IoT 卡和分佣
|
||||||
|
- 企业客户/个人客户:购买 IoT 卡套餐、管理设备、购买号卡
|
||||||
|
- 运营商:号卡订单回传和套餐管理
|
||||||
|
- 运营人员:通过设备维度批量管理投诉和代理要求,查看绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
### Goals (本阶段目标)
|
||||||
|
|
||||||
|
1. **设计完整的数据模型**:
|
||||||
|
- 定义核心实体:IoT 卡、设备、号卡、套餐、订单、代理分佣
|
||||||
|
- 绘制 ERD(实体关系图)
|
||||||
|
- 设计数据库表结构和 Schema
|
||||||
|
- 实现 GORM 模型定义
|
||||||
|
|
||||||
|
2. **支持核心业务流程**:
|
||||||
|
- 平台自营和代理分销模式(仅 IoT 卡)
|
||||||
|
- 套餐购买订单流程(单卡套餐、设备级套餐)
|
||||||
|
- 号卡运营商订单回传和虚拟商品编码映射
|
||||||
|
- 多级代理分佣计算(组合分佣 OR 条件)
|
||||||
|
- 设备与 IoT 卡的多对多绑定关系(1 设备绑定 1-4 张 IoT 卡)
|
||||||
|
- 设备级套餐流量共享机制
|
||||||
|
|
||||||
|
3. **遵循项目规范**:
|
||||||
|
- 无数据库外键约束
|
||||||
|
- 无 GORM ORM 关联标签(`foreignKey`, `references`, `hasMany`, `belongsTo` 等)
|
||||||
|
- 所有字段显式指定 `column:` 标签
|
||||||
|
- 字段类型和长度明确定义
|
||||||
|
- 所有字段添加中文注释
|
||||||
|
|
||||||
|
4. **预留扩展能力**:
|
||||||
|
- 支持未来集成 Gateway 项目(IoT 卡状态查询、停复机操作等)
|
||||||
|
- 支持未来的计费和供应管理集成
|
||||||
|
|
||||||
|
### Non-Goals (明确排除)
|
||||||
|
|
||||||
|
- ❌ API 层设计(Handlers、路由、中间件)
|
||||||
|
- ❌ 业务逻辑层设计(Services、业务规则实现)
|
||||||
|
- ❌ 计费系统实现(Billing Engine)
|
||||||
|
- ❌ 供应管理集成(Provisioning)
|
||||||
|
- ❌ 事件系统集成(Events、消息队列)
|
||||||
|
- ❌ 单元测试和集成测试
|
||||||
|
- ❌ API 文档生成
|
||||||
|
- ❌ Gateway 项目集成的具体实现(只设计数据模型字段预留)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 决策 1: 无外键约束的数据模型设计
|
||||||
|
|
||||||
|
**选择**: 所有表之间不使用数据库外键约束,通过存储关联 ID 字段手动维护关系。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 遵循项目既定规范(参考 `CLAUDE.md` 数据库设计原则)
|
||||||
|
- 提高灵活性:业务逻辑完全在代码中控制
|
||||||
|
- 提升性能:无数据库层面的引用完整性检查开销
|
||||||
|
- 分布式友好:在微服务和分布式数据库场景下更易扩展
|
||||||
|
- 简化迁移:数据库 schema 更简单,迁移更容易
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 使用外键约束:会引入数据库层面的复杂性,限制灵活性,不符合项目规范
|
||||||
|
|
||||||
|
**实施细节**:
|
||||||
|
- 所有关联关系通过 `{entity}_id` 字段存储(如 `user_id`, `agent_id`, `device_id`)
|
||||||
|
- GORM 模型不使用 `foreignKey`, `references`, `hasMany`, `belongsTo` 等标签
|
||||||
|
- 关联数据查询在 Service 层显式执行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 2: 平台自营和代理分销的统一建模
|
||||||
|
|
||||||
|
**选择**: 使用 `owner_type` 和 `owner_id` 字段统一建模平台自营和代理分销(仅 IoT 卡)。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- IoT 卡既可以平台自营销售,也可以分销给代理
|
||||||
|
- 设备不在卡管系统中销售,主要用于用户设备管理和运营人员管理投诉
|
||||||
|
- 使用多态关联字段避免为平台和代理创建两套库存系统
|
||||||
|
- 简化查询逻辑:通过 `owner_type` 区分所有者类型
|
||||||
|
|
||||||
|
**字段设计**:
|
||||||
|
```
|
||||||
|
owner_type: VARCHAR(20) -- 值: "platform"-平台 | "agent"-代理 | "user"-用户 | "device"-设备
|
||||||
|
owner_id: BIGINT -- 平台(0)、代理用户 ID、用户 ID 或设备 ID
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 分别设计 `platform_inventory` 和 `agent_inventory` 表:重复代码,增加维护成本
|
||||||
|
- ❌ 只用 `agent_id` 并用 `NULL` 表示平台:语义不清晰,查询复杂
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 3: 号卡虚拟商品编码的设计
|
||||||
|
|
||||||
|
**选择**: 在 `number_cards` 表中增加 `virtual_product_code` 字段,用于映射运营商回传订单。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 号卡本身不是系统内真实的库存商品,而是运营商侧的订单
|
||||||
|
- 需要一个"假的商品编码"来对应上游回调订单的商品标识
|
||||||
|
- 虚拟编码作为号卡和运营商订单的桥梁
|
||||||
|
|
||||||
|
**字段设计**:
|
||||||
|
```
|
||||||
|
virtual_product_code: VARCHAR(100) UNIQUE -- 虚拟商品编码,用于对应运营商订单
|
||||||
|
carrier_order_id: VARCHAR(255) -- 运营商订单 ID
|
||||||
|
carrier_product_id: VARCHAR(100) -- 运营商商品 ID
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 直接使用运营商商品 ID:缺乏系统内部的统一标识
|
||||||
|
- ❌ 创建独立的商品表:号卡不是真实库存,不应与网卡/设备商品化混淆
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 4: 设备与 IoT 卡的多对多绑定关系
|
||||||
|
|
||||||
|
**选择**: 使用中间表 `device_sim_bindings` 管理设备与 IoT 卡的绑定关系。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 一个设备可以绑定 1-4 张 IoT 卡(多对多关系)
|
||||||
|
- 中间表可以记录绑定时间、绑定状态、插槽位置等元数据
|
||||||
|
- 支持历史绑定记录查询
|
||||||
|
- 支持设备级套餐购买(套餐分配到所有绑定的 IoT 卡,流量共享)
|
||||||
|
|
||||||
|
**表设计**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE device_sim_bindings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
device_id BIGINT NOT NULL, -- 设备 ID
|
||||||
|
iot_card_id BIGINT NOT NULL, -- IoT 卡 ID
|
||||||
|
slot_position INT, -- 插槽位置 (1, 2, 3, 4)
|
||||||
|
bind_status INT DEFAULT 1, -- 绑定状态 1-已绑定 2-已解绑
|
||||||
|
bind_time TIMESTAMP, -- 绑定时间
|
||||||
|
unbind_time TIMESTAMP, -- 解绑时间
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 在设备表存储 `iot_card_ids` JSON 字段:难以查询和维护,不支持元数据
|
||||||
|
- ❌ 在 IoT 卡表存储 `device_id`:只能支持一对一,不支持多卡绑定
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 5: 代理树形结构的设计
|
||||||
|
|
||||||
|
**选择**: 在 `agent_hierarchies` 表中使用 `agent_id` + `parent_agent_id` 表示树形关系。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 每个代理只有一个上级(单亲树)
|
||||||
|
- 使用递归查询(CTE)可以获取整个代理链
|
||||||
|
- 支持计算多级分佣
|
||||||
|
|
||||||
|
**表设计**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE agent_hierarchies (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL UNIQUE, -- 代理用户 ID
|
||||||
|
parent_agent_id BIGINT, -- 上级代理用户 ID (NULL 表示顶级代理)
|
||||||
|
level INT NOT NULL, -- 代理层级 (1, 2, 3...)
|
||||||
|
path VARCHAR(500), -- 代理路径 (如: "1/5/12")
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 使用闭包表(Closure Table):过度设计,查询性能提升不明显
|
||||||
|
- ❌ 使用嵌套集合(Nested Set):插入和移动节点复杂,不适合频繁变更
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 6: 订单类型的统一建模
|
||||||
|
|
||||||
|
**选择**: 使用 `order_type` 字段区分两种订单类型,使用独立字段关联订单来源。
|
||||||
|
|
||||||
|
**订单类型**:
|
||||||
|
1. **套餐订单** (`order_type = 1`): 用户为 IoT 卡或设备购买套餐
|
||||||
|
- **单卡套餐订单**: `iot_card_id` 有值,`device_id` 为 NULL
|
||||||
|
- **设备级套餐订单**: `device_id` 有值,`iot_card_id` 为 NULL(套餐分配到所有绑定的 IoT 卡,流量共享)
|
||||||
|
2. **号卡订单** (`order_type = 2`): 运营商回传订单,`number_card_id` 有值
|
||||||
|
|
||||||
|
**字段设计**:
|
||||||
|
```
|
||||||
|
order_type: INT -- 值: 1-套餐订单 2-号卡订单
|
||||||
|
iot_card_id: BIGINT -- IoT 卡 ID(单卡套餐订单时有值)
|
||||||
|
device_id: BIGINT -- 设备 ID(设备级套餐订单时有值)
|
||||||
|
number_card_id: BIGINT -- 号卡 ID(号卡订单时有值)
|
||||||
|
package_id: BIGINT -- 套餐 ID(套餐订单时有值)
|
||||||
|
```
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 简化订单类型,只保留实际需要的两种订单类型
|
||||||
|
- 移除 SIM 卡销售订单(IoT 卡不单独销售,只通过套餐订单管理)
|
||||||
|
- 通过独立字段明确关联不同业务实体,比多态字段更清晰
|
||||||
|
- 支持设备级套餐订单,流量共享机制
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 创建 `package_orders`, `number_card_orders` 两张表:代码重复,维护成本高
|
||||||
|
- ❌ 使用 `source_type` + `source_id` 多态字段:不够清晰,查询复杂
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 7: IoT 卡状态字段预留 Gateway 集成
|
||||||
|
|
||||||
|
**选择**: 在 `iot_cards` 表中增加状态相关字段,但不在本阶段实现 Gateway 集成。
|
||||||
|
|
||||||
|
**字段设计**:
|
||||||
|
```
|
||||||
|
iccid: VARCHAR(50) UNIQUE -- IoT 卡 ICCID(唯一标识)
|
||||||
|
activation_status: INT -- 激活状态 (0-未激活 1-已激活)
|
||||||
|
real_name_status: INT -- 实名状态 (0-未实名 1-已实名)
|
||||||
|
network_status: INT -- 网络状态 (0-停机 1-开机)
|
||||||
|
data_usage_mb: BIGINT DEFAULT 0 -- 累计流量使用(MB)
|
||||||
|
last_sync_time: TIMESTAMP -- 最后一次与 Gateway 同步时间
|
||||||
|
```
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 本阶段只设计数据模型,不实现具体的 Gateway 集成逻辑
|
||||||
|
- 预留字段便于后续 Service 层调用 Gateway HTTP 接口并更新这些字段
|
||||||
|
- 这些字段的数据来源是 Gateway 项目,不由本系统直接管理
|
||||||
|
- IoT 卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法,统一使用 IoT 卡命名)
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 不预留字段:后续集成需要修改表结构,涉及数据迁移
|
||||||
|
- ❌ 在独立的 `iot_card_status` 表:过度规范化,增加查询复杂度
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 8: 字段命名和类型规范
|
||||||
|
|
||||||
|
**选择**: 严格遵循项目规范,所有字段显式指定 `column:` 标签,类型和长度明确定义。
|
||||||
|
|
||||||
|
**命名规范**:
|
||||||
|
- 数据库字段名:snake_case (如 `user_id`, `created_at`)
|
||||||
|
- Go 结构体字段名:PascalCase (如 `UserID`, `CreatedAt`)
|
||||||
|
- 必须显式指定 `gorm:"column:字段名"` 标签
|
||||||
|
|
||||||
|
**类型规范**:
|
||||||
|
- ID 字段:BIGINT (对应 Go `uint` 或 `int64`)
|
||||||
|
- 短文本:VARCHAR(50-255)
|
||||||
|
- 长文本:TEXT
|
||||||
|
- 货币金额:DECIMAL(18,2) 或 BIGINT(分为单位)
|
||||||
|
- 时间:TIMESTAMP (对应 Go `time.Time`)
|
||||||
|
- 枚举:INT 或 VARCHAR,配合常量定义
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```go
|
||||||
|
type IotCard struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;comment:IoT 卡 ID" json:"id"`
|
||||||
|
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex;not null;comment:ICCID" json:"iccid"`
|
||||||
|
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-在库 2-已分销 3-已激活" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 9: 行业卡无需实名认证的设计
|
||||||
|
|
||||||
|
**选择**: 在 IoT 卡实体中增加 `card_category` 字段(枚举值:"normal"-普通卡 | "industry"-行业卡),行业卡可以在实名状态为 0(未实名)的情况下激活和使用。
|
||||||
|
|
||||||
|
**业务规则**:
|
||||||
|
- **普通卡(normal)**: 必须完成实名认证(`real_name_status` 为 1)才能激活使用,遵循运营商实名制要求
|
||||||
|
- **行业卡(industry)**: 不需要实名认证,可以在 `real_name_status` 为 0 的情况下激活使用,适用于企业/行业客户批量采购场景
|
||||||
|
|
||||||
|
**分佣解冻规则调整**:
|
||||||
|
- **一次性分佣**: 普通卡需要实名认证后才能解冻;行业卡无需实名认证,只需满足激活和充值条件
|
||||||
|
- **长期分佣**: 普通卡需要实名认证后才能开始长期分佣;行业卡无需实名认证,满足其他条件即可
|
||||||
|
- **组合分佣**: 行业卡的时间点条件从激活时开始计算(不是实名时)
|
||||||
|
|
||||||
|
**轮询控制**:
|
||||||
|
- 行业卡的实名状态检查轮询应该被禁用或设置为低优先级
|
||||||
|
- 行业卡的流量检查和套餐检查与普通卡相同
|
||||||
|
|
||||||
|
**数据模型变更**:
|
||||||
|
```go
|
||||||
|
type IotCard struct {
|
||||||
|
// ... 其他字段 ...
|
||||||
|
CardCategory string `gorm:"column:card_category;type:varchar(20);default:'normal';comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
|
||||||
|
RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;comment:实名状态 0-未实名 1-已实名 (行业卡可以保持 0)" json:"real_name_status"`
|
||||||
|
// ... 其他字段 ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 符合企业/行业客户批量采购场景的实际需求
|
||||||
|
- 简化行业卡的激活流程,提高用户体验
|
||||||
|
- 分佣解冻逻辑需要区分普通卡和行业卡,避免行业卡因未实名而无法解冻
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 为行业卡自动设置实名状态为 1:不真实,会导致数据统计错误
|
||||||
|
- ❌ 创建独立的行业卡实体:增加系统复杂度,不利于统一管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
### 风险 1: 无外键约束导致数据一致性问题
|
||||||
|
|
||||||
|
**风险**: 手动维护关联关系可能导致孤儿记录(如删除代理后,其分销的 IoT 卡 `owner_id` 仍然指向已删除的代理)。
|
||||||
|
|
||||||
|
**缓解措施**:
|
||||||
|
- 在 Service 层实现软删除(soft delete),不物理删除关键实体
|
||||||
|
- 在删除操作前检查关联记录
|
||||||
|
- 定期运行数据一致性检查脚本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 风险 2: 设备与 IoT 卡的多对多绑定复杂度
|
||||||
|
|
||||||
|
**风险**: 中间表 `device_sim_bindings` 的状态管理复杂,可能出现一个 IoT 卡被多个设备绑定的冲突。
|
||||||
|
|
||||||
|
**缓解措施**:
|
||||||
|
- 在 Service 层实现业务规则:一个 IoT 卡同一时间只能绑定一个设备
|
||||||
|
- 在绑定前查询 IoT 卡的当前绑定状态
|
||||||
|
- 使用数据库唯一索引:`CREATE UNIQUE INDEX idx_iot_card_active_binding ON device_sim_bindings(iot_card_id) WHERE bind_status = 1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 风险 3: 号卡虚拟商品编码的唯一性冲突
|
||||||
|
|
||||||
|
**风险**: 多个号卡可能误用相同的虚拟商品编码,导致运营商订单映射错误。
|
||||||
|
|
||||||
|
**缓解措施**:
|
||||||
|
- 在 `virtual_product_code` 字段上创建唯一索引
|
||||||
|
- 在创建号卡时自动生成虚拟商品编码(使用 UUID 或业务规则生成)
|
||||||
|
- 在 Service 层校验虚拟商品编码的唯一性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 风险 4: 多级代理分佣计算性能
|
||||||
|
|
||||||
|
**风险**: 递归查询代理树获取整个分佣链可能影响性能(特别是代理层级深时)。
|
||||||
|
|
||||||
|
**缓解措施**:
|
||||||
|
- 在 `agent_hierarchies` 表中增加 `path` 字段存储代理路径(如 `"1/5/12"`),避免递归查询
|
||||||
|
- 在 Redis 中缓存代理树结构
|
||||||
|
- 使用异步任务(Asynq)计算分佣,不阻塞订单创建
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 风险 5: Gateway 集成依赖的可用性
|
||||||
|
|
||||||
|
**风险**: IoT 卡状态、流量、停复机操作依赖 Gateway 项目 HTTP 接口,如果 Gateway 不可用会影响功能。
|
||||||
|
|
||||||
|
**缓解措施**:
|
||||||
|
- 在数据库中缓存 IoT 卡状态字段,Gateway 不可用时返回缓存数据
|
||||||
|
- 设置合理的 HTTP 超时和重试机制
|
||||||
|
- 使用 Asynq 异步任务定期同步 IoT 卡状态,降低实时依赖
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Trade-off 1: 单表订单 vs 多表订单
|
||||||
|
|
||||||
|
**权衡**: 选择单表存储两种订单类型,使用 `order_type` 区分。
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 统一的订单查询和状态管理
|
||||||
|
- 代码复用度高
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 表字段较多,某些字段只对特定订单类型有意义(如 `carrier_order_id` 只对号卡订单有意义)
|
||||||
|
- 单表数据量大,可能影响查询性能
|
||||||
|
|
||||||
|
**选择理由**: 在当前业务规模下,单表方案的代码简洁性优于多表方案的性能优势。如果未来订单量巨大,可以考虑分表或分库。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Trade-off 2: 代理路径字段 vs 纯递归查询
|
||||||
|
|
||||||
|
**权衡**: 在 `agent_hierarchies` 表中增加 `path` 字段存储代理路径。
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 避免递归查询,提升查询性能
|
||||||
|
- 快速获取整个代理链
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 需要在代理关系变更时维护 `path` 字段
|
||||||
|
- 增加存储空间
|
||||||
|
|
||||||
|
**选择理由**: 分佣计算是高频操作,牺牲少量存储空间换取查询性能提升是值得的。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
### 部署步骤
|
||||||
|
|
||||||
|
1. **生成数据库迁移脚本**:
|
||||||
|
- 使用 `golang-migrate` 创建迁移脚本
|
||||||
|
- 迁移脚本位置:`migrations/` 目录
|
||||||
|
- 命名格式:`{timestamp}_create_iot_sim_tables.up.sql` 和 `.down.sql`
|
||||||
|
|
||||||
|
2. **测试环境验证**:
|
||||||
|
- 在测试数据库执行 `up` 迁移
|
||||||
|
- 验证所有表和索引创建成功
|
||||||
|
- 插入测试数据验证约束和索引
|
||||||
|
|
||||||
|
3. **生产环境部署**:
|
||||||
|
- 在生产数据库执行 `up` 迁移
|
||||||
|
- 验证表结构和索引
|
||||||
|
- 监控数据库性能
|
||||||
|
|
||||||
|
4. **GORM 模型代码部署**:
|
||||||
|
- 部署包含新 GORM 模型的代码版本
|
||||||
|
- 验证 GORM AutoMigrate 不会修改已有表结构(禁用 AutoMigrate 或仅用于开发环境)
|
||||||
|
|
||||||
|
### 回滚策略
|
||||||
|
|
||||||
|
1. **代码回滚**:
|
||||||
|
- 如果 GORM 模型有 Bug,回滚到上一个代码版本
|
||||||
|
|
||||||
|
2. **数据库回滚**:
|
||||||
|
- 执行 `.down.sql` 迁移脚本删除新创建的表
|
||||||
|
- 如果已有数据,需要先备份数据再回滚
|
||||||
|
|
||||||
|
### 数据迁移(如果需要)
|
||||||
|
|
||||||
|
- 本次为新功能,不涉及旧数据迁移
|
||||||
|
- 如果需要从旧系统导入数据,使用 ETL 脚本批量导入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions (已解决)
|
||||||
|
|
||||||
|
### ✅ 问题 1: 套餐定价和计费规则 (已解决)
|
||||||
|
|
||||||
|
**结论**:
|
||||||
|
- 套餐基本为月套餐,年套餐通过设置月数实现(如 12 个月)
|
||||||
|
- 流量单位为 MB
|
||||||
|
- **流量分为真流量和虚流量两种类型,两者共存**
|
||||||
|
- **停机判断基于虚流量**(虚流量用完后停机,即使真流量还有剩余)
|
||||||
|
- 无复杂计费规则,只有固定的套餐价格
|
||||||
|
|
||||||
|
**表设计影响**:
|
||||||
|
```
|
||||||
|
duration_months: INT -- 套餐时长(月数) 1-月套餐 12-年套餐
|
||||||
|
data_type: VARCHAR(20) -- 流量类型 "real"(真流量) | "virtual"(虚流量)
|
||||||
|
data_amount_mb: BIGINT -- 流量额度(MB)
|
||||||
|
real_data_mb: BIGINT -- 真流量额度(MB,可选)
|
||||||
|
virtual_data_mb: BIGINT -- 虚流量额度(MB,用于停机判断)
|
||||||
|
price: DECIMAL(10,2) -- 套餐价格(元)
|
||||||
|
```
|
||||||
|
|
||||||
|
**停机规则**:
|
||||||
|
- 虚流量用完后自动停机
|
||||||
|
- 真流量和虚流量独立计算,共存在套餐中
|
||||||
|
- 前端展示需要同时显示真流量和虚流量余额
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ 问题 2: 代理分佣配置方式 (已解决)
|
||||||
|
|
||||||
|
**结论**: 分佣体系非常复杂,包含多种类型和触发条件:
|
||||||
|
|
||||||
|
**分佣类型**:
|
||||||
|
1. **一次性分佣**:
|
||||||
|
- 作用于套餐系列
|
||||||
|
- 激活(实名) + 达到首次充值金额后产生
|
||||||
|
- **纯直接给钱**(固定金额,不计算差价)
|
||||||
|
- 冻结 N 天后解冻
|
||||||
|
- **一次性佣金订单必须通过钱包付款**
|
||||||
|
|
||||||
|
2. **长期分佣**:
|
||||||
|
- 作用于具体套餐
|
||||||
|
- 每个计费周期产生
|
||||||
|
- **佣金 = 实际售价 - 平台成本价**(代理看到的成本价是售价扣掉佣金)
|
||||||
|
- **号卡**:需要激活 + 充值 + 在网状态 + 三无校验(通过 Excel 导入解冻)
|
||||||
|
- **物联网卡(流量卡)**:只要用户买了就按佣金返,无需在网状态和三无校验
|
||||||
|
|
||||||
|
3. **组合分佣**:
|
||||||
|
- **物联网卡(流量卡/IoT 卡)**:
|
||||||
|
- 先产生一次性佣金
|
||||||
|
- 达到以下**任一条件**(OR 关系)后开始长期分佣:
|
||||||
|
1. 某个时间点之后(例如:实名后 3 个月)
|
||||||
|
2. **OR** 该 IoT 卡的套餐使用周期数达到阈值(例如:10 个套餐周期)
|
||||||
|
- **注意**: 套餐周期阈值是针对单张 IoT 卡的,不是设备级别
|
||||||
|
- **号卡**:
|
||||||
|
- 连续在网多少个月后开始长期分佣
|
||||||
|
|
||||||
|
**阶梯分佣**:
|
||||||
|
- **号卡**: 只有激活量作为阶梯条件
|
||||||
|
- **物联网卡(流量卡)**: 激活量 + 提货量作为阶梯条件
|
||||||
|
- 达到阶梯条件后变更分佣值
|
||||||
|
|
||||||
|
**关键业务规则**:
|
||||||
|
- 代理销售价格不能超过平台成本价的 2 倍
|
||||||
|
- **长期分佣**: 佣金 = 实际售价 - 平台成本价(阴阳菜单模式)
|
||||||
|
- **一次性佣金**: 纯直接给钱,不计算差价
|
||||||
|
- 一次性佣金订单必须通过钱包付款
|
||||||
|
|
||||||
|
**表设计影响**:
|
||||||
|
- 新增 `commission_templates` 表:分佣模板(常用分佣方案)
|
||||||
|
- 新增 `commission_rules` 表:代理分佣规则配置(需区分号卡和 IoT 卡)
|
||||||
|
- 新增 `commission_records` 表:分佣记录(冻结/解冻状态)
|
||||||
|
- 新增 `commission_ladder` 表:阶梯分佣配置(号卡只支持激活量,IoT 卡支持激活量+提货量)
|
||||||
|
- 新增 `commission_approvals` 表:分佣解冻审批
|
||||||
|
- 新增 `commission_combined_conditions` 表:组合分佣条件配置(时间点、套餐周期数、连续在网月数),**OR 关系解冻**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ 问题 3: 号卡运营商订单回传数据格式 (已解决)
|
||||||
|
|
||||||
|
**结论**:
|
||||||
|
- Gateway 项目统一转换各上游订单为 JSON 格式后回传
|
||||||
|
- 号卡资金流不经过平台,直接支付给运营商
|
||||||
|
- 平台接收运营商周期性结算的佣金总额,再分配给代理
|
||||||
|
|
||||||
|
**表设计影响**:
|
||||||
|
```
|
||||||
|
carrier_order_id: VARCHAR(255) -- 运营商订单 ID
|
||||||
|
carrier_order_data: JSONB -- 运营商订单原始数据(JSON)
|
||||||
|
settlement_status: INT -- 结算状态 1-待结算 2-已结算
|
||||||
|
settlement_amount: DECIMAL(18,2) -- 运营商结算佣金金额
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ 问题 4: IoT 卡绑定设备的插槽数量限制 (已解决)
|
||||||
|
|
||||||
|
**结论**: 一个设备最多插 4 张卡
|
||||||
|
|
||||||
|
**表设计影响**:
|
||||||
|
```
|
||||||
|
max_sim_slots: INT DEFAULT 4 -- 设备最大插槽数量(默认 4)
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务规则**: 在 Service 层校验设备当前绑定的 IoT 卡数量不超过 `max_sim_slots`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ 问题 5: Gateway 集成的认证和授权 (已解决)
|
||||||
|
|
||||||
|
**结论**: Gateway 使用统一的加密传输协议
|
||||||
|
|
||||||
|
**请求格式**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"appId": "your_app_id",
|
||||||
|
"data": "AES加密后的Base64字符串",
|
||||||
|
"sign": "MD5签名(大写)",
|
||||||
|
"timestamp": 1704067200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**加密方案**:
|
||||||
|
- 数据加密:AES-128-ECB + PKCS5Padding,密钥为 `MD5(appSecret)` 的原始字节数组
|
||||||
|
- 签名算法:MD5(appId + data + timestamp + appSecret),转大写
|
||||||
|
- 时间戳:Unix 秒级时间戳,允许 ±5 分钟误差
|
||||||
|
|
||||||
|
**配置文件影响**:
|
||||||
|
```yaml
|
||||||
|
gateway:
|
||||||
|
base_url: "https://gateway.example.com"
|
||||||
|
app_id: "your_app_id"
|
||||||
|
app_secret: "your_app_secret"
|
||||||
|
timeout: 30s
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现范围**: 本阶段只设计数据模型,不实现 Gateway 集成的具体 HTTP 客户端代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 9: 佣金提现和财务管理
|
||||||
|
|
||||||
|
**选择**: 设计独立的佣金提现申请流程和财务账户管理。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 代理需要将冻结/已发放的佣金提现到银行卡或支付宝
|
||||||
|
- 需要审批流程控制提现风险
|
||||||
|
- 需要记录提现历史和手续费
|
||||||
|
|
||||||
|
**表设计**:
|
||||||
|
```sql
|
||||||
|
-- 佣金提现申请表
|
||||||
|
CREATE TABLE commission_withdrawal_requests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL, -- 代理用户 ID
|
||||||
|
amount DECIMAL(18,2) NOT NULL, -- 提现金额
|
||||||
|
fee DECIMAL(18,2) DEFAULT 0, -- 手续费
|
||||||
|
actual_amount DECIMAL(18,2), -- 实际到账金额
|
||||||
|
withdrawal_method VARCHAR(20), -- 提现方式 "alipay" | "wechat" | "bank"
|
||||||
|
account_info JSONB, -- 收款账户信息(姓名、账号等)
|
||||||
|
status INT DEFAULT 1, -- 状态 1-待审核 2-已通过 3-已拒绝 4-已到账
|
||||||
|
approved_by BIGINT, -- 审批人用户 ID
|
||||||
|
approved_at TIMESTAMP, -- 审批时间
|
||||||
|
paid_at TIMESTAMP, -- 到账时间
|
||||||
|
reject_reason TEXT, -- 拒绝原因
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 佣金提现设置表
|
||||||
|
CREATE TABLE commission_withdrawal_settings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
min_withdrawal_amount DECIMAL(10,2), -- 最低提现金额
|
||||||
|
fee_rate DECIMAL(5,4), -- 手续费率(如 0.01 表示 1%)
|
||||||
|
arrival_days INT, -- 到账天数
|
||||||
|
is_active BOOLEAN DEFAULT TRUE, -- 是否生效(最新一条)
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 不设计提现流程:代理无法取出佣金,体验差
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 10: 商品分配和套餐系列管理
|
||||||
|
|
||||||
|
**选择**: 设计套餐系列作为套餐的分组,用于一次性分佣规则配置。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 一次性分佣作用于套餐系列,而不是单个套餐
|
||||||
|
- 套餐系列可以包含多个套餐(如"月套餐系列"包含 10GB、20GB、30GB 等月套餐)
|
||||||
|
- 便于批量管理和分佣规则配置
|
||||||
|
|
||||||
|
**表设计**:
|
||||||
|
```sql
|
||||||
|
-- 套餐系列表
|
||||||
|
CREATE TABLE package_series (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
series_name VARCHAR(255) NOT NULL, -- 系列名称
|
||||||
|
series_code VARCHAR(100) UNIQUE, -- 系列编码
|
||||||
|
description TEXT, -- 描述
|
||||||
|
status INT DEFAULT 1, -- 状态 1-启用 2-禁用
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 套餐表增加 series_id 字段
|
||||||
|
ALTER TABLE packages ADD COLUMN series_id BIGINT;
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: 套餐只适用于 IoT 卡(ICCID),用户可以为单张 IoT 卡购买套餐,也可以为设备购买套餐(套餐分配到设备绑定的所有 IoT 卡,流量设备级共享)
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 不设计套餐系列:需要为每个套餐单独配置分佣规则,维护成本高
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 11: 资产分配批量操作
|
||||||
|
|
||||||
|
**选择**: 设计批量资产分配接口,支持设备批量分配和 IoT 卡批量分配。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 代理商提货时通常批量分配大量 IoT 卡或设备
|
||||||
|
- IoT 卡如果绑定了设备,分配时需要连同设备一起分配
|
||||||
|
- 批量操作提高效率
|
||||||
|
|
||||||
|
**业务规则**:
|
||||||
|
- **设备批量分配**: 只分配设备,不影响设备绑定的 IoT 卡所有权
|
||||||
|
- **IoT 卡批量分配**: 分配 IoT 卡,如果 IoT 卡有设备信息(`device_id`),则设备和 IoT 卡一起分配
|
||||||
|
- 批量分配时需要校验数量和权限
|
||||||
|
|
||||||
|
**表设计影响**:
|
||||||
|
- 复用现有的 `iot_cards` 和 `devices` 表的 `owner_type` 和 `owner_id` 字段
|
||||||
|
- 批量操作通过 Service 层事务处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 12: 换卡申请管理
|
||||||
|
|
||||||
|
**选择**: 设计换卡申请表,记录客户的换卡请求和处理流程。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 客户的 IoT 卡损坏或丢失时需要换卡
|
||||||
|
- 需要审批流程和旧卡/新卡 ICCID 映射
|
||||||
|
- 换卡后需要转移套餐和流量余额
|
||||||
|
|
||||||
|
**表设计**:
|
||||||
|
```sql
|
||||||
|
-- 换卡申请表
|
||||||
|
CREATE TABLE card_replacement_requests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL, -- 申请用户 ID
|
||||||
|
old_iccid VARCHAR(50) NOT NULL, -- 旧卡 ICCID
|
||||||
|
new_iccid VARCHAR(50), -- 新卡 ICCID(审批时填充)
|
||||||
|
reason TEXT, -- 换卡原因
|
||||||
|
status INT DEFAULT 1, -- 状态 1-待处理 2-已通过 3-已拒绝 4-已完成
|
||||||
|
approved_by BIGINT, -- 处理人用户 ID
|
||||||
|
approved_at TIMESTAMP, -- 处理时间
|
||||||
|
completed_at TIMESTAMP, -- 完成时间(新卡激活时间)
|
||||||
|
reject_reason TEXT, -- 拒绝原因
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 不设计换卡流程:客户无法自助换卡,需要人工处理,效率低
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 13: 开发能力管理
|
||||||
|
|
||||||
|
**选择**: 设计开发能力管理表,存储 API 对接参数(AppID、AppSecret、回调地址等)。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 代理或平台需要通过 API 对接系统
|
||||||
|
- 需要管理 API 凭证和回调配置
|
||||||
|
- 支持多个应用(多套 AppID/AppSecret)
|
||||||
|
|
||||||
|
**表设计**:
|
||||||
|
```sql
|
||||||
|
-- 开发能力配置表
|
||||||
|
CREATE TABLE dev_capability_configs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL, -- 用户 ID(平台或代理)
|
||||||
|
app_name VARCHAR(255), -- 应用名称
|
||||||
|
app_id VARCHAR(100) UNIQUE, -- 应用 ID
|
||||||
|
app_secret VARCHAR(255), -- 应用密钥
|
||||||
|
callback_url VARCHAR(500), -- 回调地址
|
||||||
|
ip_whitelist TEXT, -- IP 白名单(多个 IP 用逗号分隔)
|
||||||
|
status INT DEFAULT 1, -- 状态 1-启用 2-禁用
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 不设计开发能力管理:无法支持 API 对接,限制系统扩展性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 14: 收款商户设置
|
||||||
|
|
||||||
|
**选择**: 设计收款商户设置表,存储代理的收款账户信息。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 代理提现时需要指定收款账户
|
||||||
|
- 支持多种收款方式(支付宝、微信、银行卡)
|
||||||
|
- 需要验证账户信息的真实性
|
||||||
|
|
||||||
|
**表设计**:
|
||||||
|
```sql
|
||||||
|
-- 收款商户设置表
|
||||||
|
CREATE TABLE payment_merchant_settings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL, -- 用户 ID
|
||||||
|
merchant_type VARCHAR(20), -- 商户类型 "alipay" | "wechat" | "bank"
|
||||||
|
account_name VARCHAR(255), -- 账户名称
|
||||||
|
account_number VARCHAR(255), -- 账号
|
||||||
|
bank_name VARCHAR(255), -- 银行名称(仅银行卡)
|
||||||
|
bank_branch VARCHAR(255), -- 开户行(仅银行卡)
|
||||||
|
is_verified BOOLEAN DEFAULT FALSE, -- 是否已验证
|
||||||
|
is_default BOOLEAN DEFAULT FALSE, -- 是否默认账户
|
||||||
|
status INT DEFAULT 1, -- 状态 1-启用 2-禁用
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 每次提现时填写账户信息:重复录入,用户体验差
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 15: IoT 卡轮询机制和流量管理
|
||||||
|
|
||||||
|
**选择**: 设计三个独立的轮询流程和相关的数据表支持卡流量监控和套餐流量管理。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- IoT 卡需要轮询实名状态,实名后降低轮询频率
|
||||||
|
- IoT 卡需要轮询流量使用情况,防止超额
|
||||||
|
- 设备级套餐需要汇总设备所有卡的流量,判断是否超过套餐额度
|
||||||
|
- 卡的流量轮询和套餐流量检查应该是两个独立的逻辑
|
||||||
|
- 支持细粒度的轮询配置(按运营商、按卡状态配置不同的轮询策略)
|
||||||
|
- 需要记录流量历史,便于查询和分析
|
||||||
|
|
||||||
|
**新增表设计**:
|
||||||
|
|
||||||
|
1. **运营商表 (carriers)**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE carriers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
carrier_code VARCHAR(50) UNIQUE NOT NULL, -- 运营商编码(CMCC/CUCC/CTCC)
|
||||||
|
carrier_name VARCHAR(100) NOT NULL, -- 运营商名称(中国移动/中国联通/中国电信)
|
||||||
|
description VARCHAR(500), -- 运营商描述
|
||||||
|
status INT NOT NULL DEFAULT 1, -- 状态 1-启用 2-禁用
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 初始数据
|
||||||
|
INSERT INTO carriers (carrier_code, carrier_name, status) VALUES
|
||||||
|
('CMCC', '中国移动', 1),
|
||||||
|
('CUCC', '中国联通', 1),
|
||||||
|
('CTCC', '中国电信', 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **套餐使用情况表 (package_usages)**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE package_usages (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
order_id BIGINT NOT NULL, -- 订单 ID
|
||||||
|
package_id BIGINT NOT NULL, -- 套餐 ID
|
||||||
|
usage_type VARCHAR(20) NOT NULL, -- 使用类型 single_card-单卡套餐 device-设备级套餐
|
||||||
|
iot_card_id BIGINT, -- IoT 卡 ID(单卡套餐时有值)
|
||||||
|
device_id BIGINT, -- 设备 ID(设备级套餐时有值)
|
||||||
|
data_limit_mb BIGINT NOT NULL, -- 流量限额(MB)
|
||||||
|
data_usage_mb BIGINT DEFAULT 0, -- 已使用流量(MB)
|
||||||
|
real_data_usage_mb BIGINT DEFAULT 0, -- 真流量使用(MB)
|
||||||
|
virtual_data_usage_mb BIGINT DEFAULT 0, -- 虚流量使用(MB)
|
||||||
|
activated_at TIMESTAMP NOT NULL, -- 套餐生效时间
|
||||||
|
expires_at TIMESTAMP NOT NULL, -- 套餐过期时间
|
||||||
|
status INT NOT NULL DEFAULT 1, -- 状态 1-生效中 2-已用完 3-已过期
|
||||||
|
last_package_check_at TIMESTAMP, -- 最后一次套餐流量检查时间
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_package_usages_order ON package_usages(order_id);
|
||||||
|
CREATE INDEX idx_package_usages_package ON package_usages(package_id);
|
||||||
|
CREATE INDEX idx_package_usages_iot_card ON package_usages(iot_card_id);
|
||||||
|
CREATE INDEX idx_package_usages_device ON package_usages(device_id);
|
||||||
|
CREATE INDEX idx_package_usages_check ON package_usages(status, expires_at, last_package_check_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **轮询配置表 (polling_configs)**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE polling_configs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
config_name VARCHAR(100) UNIQUE NOT NULL, -- 配置名称(如 未实名卡、实名卡)
|
||||||
|
description VARCHAR(500), -- 配置描述
|
||||||
|
card_condition VARCHAR(50), -- 卡状态条件(not_real_name | real_name | activated | suspended)
|
||||||
|
carrier_id BIGINT, -- 运营商 ID(NULL 表示所有运营商)
|
||||||
|
real_name_check_enabled BOOLEAN DEFAULT false, -- 是否启用实名检查
|
||||||
|
real_name_check_interval INT DEFAULT 60, -- 实名检查间隔(秒)
|
||||||
|
card_data_check_enabled BOOLEAN DEFAULT false, -- 是否启用卡流量检查
|
||||||
|
card_data_check_interval INT DEFAULT 60, -- 卡流量检查间隔(秒)
|
||||||
|
package_check_enabled BOOLEAN DEFAULT false, -- 是否启用套餐流量检查
|
||||||
|
package_check_interval INT DEFAULT 60, -- 套餐流量检查间隔(秒)
|
||||||
|
priority INT NOT NULL DEFAULT 100, -- 优先级(数字越小优先级越高)
|
||||||
|
status INT NOT NULL DEFAULT 1, -- 状态 1-启用 2-禁用
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_polling_configs_match ON polling_configs(status, card_condition, carrier_id, priority);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **流量使用记录表 (data_usage_records)**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE data_usage_records (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
iot_card_id BIGINT NOT NULL, -- IoT 卡 ID
|
||||||
|
data_usage_mb BIGINT NOT NULL, -- 流量使用量(MB)
|
||||||
|
data_increase_mb BIGINT DEFAULT 0, -- 相比上次的增量(MB)
|
||||||
|
check_time TIMESTAMP NOT NULL, -- 检查时间
|
||||||
|
source VARCHAR(50) DEFAULT 'polling', -- 数据来源(polling-轮询 manual-手动 gateway-回调)
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_data_usage_records_card_time ON data_usage_records(iot_card_id, check_time DESC);
|
||||||
|
CREATE INDEX idx_data_usage_records_time ON data_usage_records(check_time);
|
||||||
|
```
|
||||||
|
|
||||||
|
**IoT 卡表调整**:
|
||||||
|
```sql
|
||||||
|
-- 添加以下字段
|
||||||
|
carrier_id BIGINT NOT NULL, -- 运营商 ID(关联 carriers 表)
|
||||||
|
enable_polling BOOLEAN DEFAULT true, -- 是否参与轮询(true-参与 false-不参与)
|
||||||
|
last_data_check_at TIMESTAMP, -- 最后一次流量检查时间
|
||||||
|
last_real_name_check_at TIMESTAMP, -- 最后一次实名检查时间
|
||||||
|
|
||||||
|
-- 添加索引
|
||||||
|
CREATE INDEX idx_iot_cards_carrier ON iot_cards(carrier_id);
|
||||||
|
CREATE INDEX idx_iot_cards_data_check ON iot_cards(enable_polling, activation_status, last_data_check_at);
|
||||||
|
CREATE INDEX idx_iot_cards_real_name_check ON iot_cards(enable_polling, real_name_status, last_real_name_check_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
**轮询逻辑设计**:
|
||||||
|
|
||||||
|
1. **实名状态轮询**:
|
||||||
|
- 查询需要检查实名的卡(根据 polling_configs 匹配条件)
|
||||||
|
- 调用 Gateway API 获取卡的实名状态
|
||||||
|
- 更新 iot_cards.real_name_status 和 last_real_name_check_at
|
||||||
|
- 实名通过后降低轮询频率(通过配置表实现梯度策略)
|
||||||
|
|
||||||
|
2. **卡流量轮询**:
|
||||||
|
- 只轮询有生效套餐的卡(通过 package_usages 表 JOIN 查询)
|
||||||
|
- 卡必须 enable_polling = true
|
||||||
|
- 调用 Gateway API 获取卡的实时流量
|
||||||
|
- 更新 iot_cards.data_usage_mb 和 last_data_check_at
|
||||||
|
- 插入流量使用记录到 data_usage_records 表
|
||||||
|
|
||||||
|
3. **套餐流量检查**:
|
||||||
|
- 查询需要检查的套餐使用记录(status = 1 且未过期)
|
||||||
|
- 单卡套餐:直接读取关联卡的 data_usage_mb
|
||||||
|
- 设备级套餐:汇总设备所有卡的 data_usage_mb
|
||||||
|
- 更新 package_usages.data_usage_mb 和 last_package_check_at
|
||||||
|
- 判断是否超额(data_usage_mb >= data_limit_mb)
|
||||||
|
- 如果超额:调用 Gateway 停机(单卡停单卡,设备停所有卡)
|
||||||
|
|
||||||
|
**配置示例**:
|
||||||
|
```
|
||||||
|
┌─────┬──────────────┬───────────────┬─────────────┬──────────┬──────────┬────────────┬────────────┬──────────┬──────────┬────────┐
|
||||||
|
│ ID │ 配置名称 │ 卡状态 │ 运营商 ID │ 实名检查 │ 实名间隔 │ 卡流量检查 │ 卡流量间隔 │ 套餐检查 │ 套餐间隔 │ 优先级 │
|
||||||
|
├─────┼──────────────┼───────────────┼─────────────┼──────────┼──────────┼────────────┼────────────┼──────────┼──────────┼────────┤
|
||||||
|
│ 1 │ 未实名移动卡 │ not_real_name │ 1 (移动) │ ✅ │ 60秒 │ ❌ │ - │ ❌ │ - │ 10 │
|
||||||
|
├─────┼──────────────┼───────────────┼─────────────┼──────────┼──────────┼────────────┼────────────┼──────────┼──────────┼────────┤
|
||||||
|
│ 2 │ 未实名联通卡 │ not_real_name │ 2 (联通) │ ✅ │ 120秒 │ ❌ │ - │ ❌ │ - │ 11 │
|
||||||
|
├─────┼──────────────┼───────────────┼─────────────┼──────────┼──────────┼────────────┼────────────┼──────────┼──────────┼────────┤
|
||||||
|
│ 3 │ 实名卡-通用 │ real_name │ NULL (所有) │ ✅ │ 3600秒 │ ✅ │ 60秒 │ ✅ │ 60秒 │ 20 │
|
||||||
|
└─────┴──────────────┴───────────────┴─────────────┴──────────┴──────────┴────────────┴────────────┴──────────┴──────────┴────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务优势**:
|
||||||
|
- 套餐为核心:所有流量业务围绕 package_usages 表,清晰明确
|
||||||
|
- 灵活的轮询配置:通过 polling_configs 表动态配置,不需要改代码
|
||||||
|
- 梯度配置:未实名卡和实名卡使用不同的轮询策略
|
||||||
|
- 细粒度控制:支持按运营商配置,支持手动禁用特定卡的轮询
|
||||||
|
- 流量历史:data_usage_records 表记录所有流量检查历史,便于分析
|
||||||
|
- 性能优化:只轮询有套餐的卡,通过 enable_polling 避免无效轮询
|
||||||
|
- 独立流程:实名轮询、卡流量轮询、套餐流量检查三个独立流程,互不干扰
|
||||||
|
|
||||||
|
**数据保留策略**:
|
||||||
|
- 流量使用记录表(data_usage_records)数据量会快速增长
|
||||||
|
- 建议定期清理 90 天前的记录,或使用 PostgreSQL 分区表
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- ❌ 在设备表直接跟踪流量:设备和卡的逻辑应该独立,套餐才是业务核心
|
||||||
|
- ❌ 不区分卡流量轮询和套餐流量检查:混在一起会导致逻辑复杂,难以维护
|
||||||
|
- ❌ 使用固定的轮询频率:无法支持梯度策略,无法针对不同运营商优化
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
构建 IoT 卡管理系统来支持三大核心业务:IoT 卡(物联网卡/流量卡)、设备(Device)、号卡(NumberCard)的全生命周期管理。系统需要支持平台自营和多级代理商分销模式、套餐订购流程和运营商订单回传处理,实现从产品分销到分佣结算的完整业务闭环。
|
||||||
|
|
||||||
|
**核心概念澄清**:
|
||||||
|
- **IoT 卡** = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)
|
||||||
|
- **普通卡**: 需要实名认证才能激活使用,遵循运营商实名制要求
|
||||||
|
- **行业卡**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景
|
||||||
|
- **设备**:用户的物联网设备(如 GPS 追踪器、智能传感器),可绑定 1-4 张 IoT 卡,主要用于批量管理和设备操作(重启、修改密码等),不在卡管系统中销售
|
||||||
|
- **号卡**:完全独立的业务线,从上游平台下单,不走我们平台激活和充值,只接收订单状态更新
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增 IoT 卡(IotCard)业务模型:支持 IoT 卡库存管理、平台自营销售、代理分销(分配)、套餐购买订单生成、集成 Gateway 项目 HTTP 接口获取卡状态/实名状态/流量详情/停复机操作等能力
|
||||||
|
- 新增设备(Device)业务模型:支持用户设备管理、与 IoT 卡的绑定关系(1设备绑定1-4张IoT卡)、设备批量分配、设备操作(重启、修改密码、重置等)
|
||||||
|
- 新增号卡(NumberCard)业务模型:支持运营商订单回传、虚拟商品编码映射、号卡代理分销和分佣、运营商侧套餐管理
|
||||||
|
- 新增套餐(Package)管理:支持 IoT 卡套餐定义、套餐系列、真流量/虚流量共存机制、套餐订购流程、设备级套餐(流量共享)
|
||||||
|
- 新增订单(Order)管理:支持两种订单类型(套餐订单、号卡订单)、订单状态流转、设备级套餐订单
|
||||||
|
- 新增多级代理商分佣体系:支持树形代理关系(每个代理只有一个上级)、三种分佣类型(一次性/长期/组合)、分佣计算逻辑、梯度佣金、分佣解冻和审批流程
|
||||||
|
- 集成现有用户体系:复用已有的平台用户、代理用户、企业用户、个人用户模型
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
#### 核心数据模型
|
||||||
|
- `iot-card`: IoT 卡业务模型 - 定义 IoT 卡实体(物联网卡/流量卡)、卡业务类型(普通卡/行业卡,card_category)、状态、库存管理、平台自营和代理分销规则、Gateway 项目集成(状态/实名/流量/停复机)、运营商关联(carrier_id)、轮询控制字段(enable_polling、last_data_check_at、last_real_name_check_at)、行业卡无需实名认证规则
|
||||||
|
- `iot-device`: 设备业务模型 - 定义设备实体、用户设备管理、IoT 卡绑定关系(1设备绑定1-4张IoT卡)、设备操作接口(重启/修改密码/重置)、设备批量分配
|
||||||
|
- `iot-number-card`: 号卡业务模型 - 定义号卡实体、虚拟商品编码、运营商订单映射、代理分销和分佣规则(下单即冻结、次月导入Excel解冻)
|
||||||
|
- `iot-package`: 套餐管理 - 定义套餐实体(只适用于IoT卡)、套餐系列关联(series_id)、真流量/虚流量共存机制(real_data_mb+virtual_data_mb)、停机判断规则(基于虚流量)、设备级套餐(流量共享)
|
||||||
|
- `iot-order`: 订单管理 - 定义订单实体、订单类型(1-套餐订单 2-号卡订单)、订单状态流转、设备级套餐订单支持
|
||||||
|
- `iot-agent-commission`: 代理分佣 - 定义代理树形关系、分佣规则(一次性/长期/组合,series_id用于一次性分佣,package_id用于长期分佣)、分佣计算逻辑、梯度佣金(号卡:激活量;IoT卡:激活量+提货量)、分佣解冻条件(行业卡无需实名认证即可解冻),组合佣金:时间点 OR 套餐周期阈值、结算流程
|
||||||
|
|
||||||
|
#### 财务和账户管理
|
||||||
|
- `iot-commission-withdrawal`: 佣金提现管理 - 代理佣金提现申请、审批流程、提现记录查询
|
||||||
|
- `iot-commission-withdrawal-settings`: 佣金提现设置 - 提现参数配置(最低金额、手续费率、到账时间等)
|
||||||
|
- `iot-financial-account`: 我的账户 - 查询当前登录账号的佣金数据(可提现余额、冻结金额、累计收入等)
|
||||||
|
- `iot-payment-merchant-settings`: 收款商户设置 - 配置支付参数(支付宝、微信等收款账户)
|
||||||
|
- `iot-dev-capability-management`: 开发能力管理 - 管理 API 对接参数(AppID、AppSecret、回调地址等)
|
||||||
|
- `iot-commission-template-management`: 分佣模板管理 - 创建和管理分佣模板,快速为代理分配产品时设置佣金规则
|
||||||
|
|
||||||
|
#### 商品管理
|
||||||
|
- `iot-number-card-management`: 号卡管理 - 新增和管理号卡商品基础信息(虚拟商品编码、运营商、套餐类型等)
|
||||||
|
- `iot-number-card-allocation`: 号卡分配 - 为特定代理分配号卡商品,设置佣金模式(一次性/长期/组合)
|
||||||
|
- `iot-package-series-management`: 套餐系列管理 - 新增和管理套餐系列(用于分组和佣金规则配置)
|
||||||
|
- `iot-package-management`: 套餐管理 - 新增和管理套餐(只能看到自己的套餐;管理员可以看到全部)
|
||||||
|
- `iot-package-allocation`: 套餐分配 - 为直属下级代理分配套餐,设置佣金模式
|
||||||
|
|
||||||
|
#### 资产管理
|
||||||
|
- `iot-single-card-info`: 单卡信息查询 - 通过 ICCID 查询单卡详细信息,提供操作入口(套餐充值、停复机、流量详情、更改过期时间、转新卡、停复机记录、往期订单、增减流量、变更钱包余额、充值支付密码、续充、设备操作)
|
||||||
|
- `iot-card-asset-management`: IoT 卡资产管理 - 查询 IoT 卡信息,提供批量操作入口(批量分配、批量激活、批量停复机等)
|
||||||
|
- `iot-device-asset-management`: 设备资产管理 - 查看设备信息,提供操作入口,查看和修改设备绑定的 IoT 卡信息,执行设备相关操作(重启、修改密码、重置)
|
||||||
|
- `iot-asset-allocation`: 资产分配 - 为特定代理批量分配 IoT 卡或设备(支持设备批量分配和 IoT 卡批量分配;设备分配时自动分配绑定的所有 IoT 卡)
|
||||||
|
- `iot-card-replacement-request`: 换卡申请管理 - 客户提交的换卡申请管理,处理换卡申请,填充新的 ICCID
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
无 - 本次变更为新增能力,不修改现有能力的需求。已有的用户体系(`user-organization`, `auth`, `role-permission`)将被复用,但不修改其规范。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
**新增数据模型**:
|
||||||
|
- 运营商(Carrier)表及 GORM 模型 - 运营商基础信息(中国移动、中国联通、中国电信)
|
||||||
|
- IoT 卡(IotCard)表及 GORM 模型 - 物联网卡/流量卡的统一管理
|
||||||
|
- 设备(Device)表及 GORM 模型 - 用户设备管理
|
||||||
|
- 设备-IoT卡绑定关系(DeviceSimBinding)表及 GORM 模型
|
||||||
|
- 号卡(NumberCard)表及 GORM 模型
|
||||||
|
- 套餐系列(PackageSeries)表及 GORM 模型
|
||||||
|
- 套餐(Package)表及 GORM 模型
|
||||||
|
- 代理套餐分配(AgentPackageAllocation)表及 GORM 模型
|
||||||
|
- 套餐使用情况(PackageUsage)表及 GORM 模型 - 跟踪单卡套餐和设备级套餐的流量使用
|
||||||
|
- 轮询配置(PollingConfig)表及 GORM 模型 - 支持梯度轮询策略(实名检查、卡流量检查、套餐流量检查)
|
||||||
|
- 流量使用记录(DataUsageRecord)表及 GORM 模型 - 记录卡的流量历史,支持流量查询和分析
|
||||||
|
- 订单(Order)表及 GORM 模型
|
||||||
|
- 代理层级关系(AgentHierarchy)表及 GORM 模型
|
||||||
|
- 分佣规则(CommissionRule)表及 GORM 模型
|
||||||
|
- 阶梯分佣配置(CommissionLadder)表及 GORM 模型
|
||||||
|
- 组合分佣条件(CommissionCombinedCondition)表及 GORM 模型
|
||||||
|
- 分佣记录(CommissionRecord)表及 GORM 模型
|
||||||
|
- 分佣审批(CommissionApproval)表及 GORM 模型
|
||||||
|
- 分佣模板(CommissionTemplate)表及 GORM 模型
|
||||||
|
- 号卡运营商结算(CarrierSettlement)表及 GORM 模型
|
||||||
|
- 佣金提现申请(CommissionWithdrawalRequest)表及 GORM 模型
|
||||||
|
- 佣金提现设置(CommissionWithdrawalSetting)表及 GORM 模型
|
||||||
|
- 收款商户设置(PaymentMerchantSetting)表及 GORM 模型
|
||||||
|
- 开发能力配置(DevCapabilityConfig)表及 GORM 模型
|
||||||
|
- 换卡申请(CardReplacementRequest)表及 GORM 模型
|
||||||
|
|
||||||
|
**系统集成**:
|
||||||
|
- 依赖现有用户体系(`user_organizations`, `users`, `roles`, `permissions` 等表)
|
||||||
|
- 需要支持三个前端入口:Web 后台(平台+代理)、H5代理/企业端、H5客户端
|
||||||
|
- 集成 Gateway 项目 HTTP 接口:SIM 卡状态查询、实名状态查询、流量详情查询、停复机操作等
|
||||||
|
|
||||||
|
**业务流程**:
|
||||||
|
- IoT 卡的平台自营销售流程和代理分销流程
|
||||||
|
- 设备的用户管理流程(添加设备、绑定IoT卡、设备操作)
|
||||||
|
- 设备的批量分配流程(运营人员分配设备给代理,自动分配绑定的所有IoT卡)
|
||||||
|
- 套餐购买订单流程(单卡套餐订单、设备级套餐订单)
|
||||||
|
- 设备级套餐流量共享机制(套餐分配到设备绑定的所有IoT卡,流量共享)
|
||||||
|
- 号卡的虚拟商品编码映射和运营商订单回传
|
||||||
|
- 多级代理分佣计算和结算流程:
|
||||||
|
- IoT 卡分佣:一次性佣金(实名+充值+购买套餐)、长期佣金(购买套餐)、组合佣金(时间点 OR 套餐周期阈值)
|
||||||
|
- 号卡分佣:下单即冻结,次月导入Excel解冻
|
||||||
|
- 分佣解冻和审批流程
|
||||||
|
|
||||||
|
**明确排除的范围**(本阶段不涉及):
|
||||||
|
- API 层(Handlers)
|
||||||
|
- 业务逻辑层(Services)
|
||||||
|
- 计费系统实现(Billing Engine)
|
||||||
|
- 供应管理集成(Provisioning)
|
||||||
|
- 事件系统集成(Events)
|
||||||
|
- 单元测试和集成测试
|
||||||
|
- API 文档生成
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 代理树形关系
|
||||||
|
|
||||||
|
系统 SHALL 管理代理的树形层级关系,每个代理只有一个上级代理。
|
||||||
|
|
||||||
|
**agent_hierarchies 表**:
|
||||||
|
- `id`: 代理关系 ID(主键,BIGINT)
|
||||||
|
- `agent_id`: 代理用户 ID(BIGINT,唯一)
|
||||||
|
- `parent_agent_id`: 上级代理用户 ID(BIGINT,可空,NULL 表示顶级代理)
|
||||||
|
- `level`: 代理层级(INT,1-顶级代理 2-二级代理 ...)
|
||||||
|
- `path`: 代理路径(VARCHAR(500),如 "1/5/12",用于快速获取整个代理链)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建顶级代理
|
||||||
|
|
||||||
|
- **WHEN** 平台创建顶级代理(用户 ID 为 101)
|
||||||
|
- **THEN** 系统创建代理关系记录,`agent_id` 为 101,`parent_agent_id` 为 NULL,`level` 为 1,`path` 为 "101"
|
||||||
|
|
||||||
|
#### Scenario: 创建下级代理
|
||||||
|
|
||||||
|
- **WHEN** 顶级代理(ID 为 101)创建下级代理(用户 ID 为 102)
|
||||||
|
- **THEN** 系统创建代理关系记录,`agent_id` 为 102,`parent_agent_id` 为 101,`level` 为 2,`path` 为 "101/102"
|
||||||
|
|
||||||
|
#### Scenario: 查询代理的整个上级链
|
||||||
|
|
||||||
|
- **WHEN** 查询代理(ID 为 103,路径为 "101/102/103")的上级链
|
||||||
|
- **THEN** 系统解析 `path` 字段,返回代理 101(顶级)、102(父级)、103(当前代理)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣规则配置
|
||||||
|
|
||||||
|
系统 SHALL 支持为代理配置分佣规则,包括一次性分佣、长期分佣和组合分佣。
|
||||||
|
|
||||||
|
**commission_rules 表**:
|
||||||
|
- `id`: 分佣规则 ID(主键,BIGINT)
|
||||||
|
- `agent_id`: 代理用户 ID(BIGINT)
|
||||||
|
- `business_type`: 业务类型(VARCHAR(20),"iot_card"-IoT卡 | "number_card"-号卡)
|
||||||
|
- `commission_type`: 分佣类型(VARCHAR(20),"one_time"-一次性 | "long_term"-长期 | "combined"-组合)
|
||||||
|
- `series_id`: 套餐系列 ID(BIGINT,可空,**仅一次性分佣使用**,关联 package_series 表)
|
||||||
|
- `package_id`: 套餐 ID(BIGINT,可空,**仅长期分佣使用**,关联 packages 表)
|
||||||
|
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed"-固定金额 | "percent"-百分比)
|
||||||
|
- `commission_value`: 分佣值(DECIMAL(10,4),固定金额或百分比值)
|
||||||
|
- `freeze_days`: 冻结天数(INT,分佣冻结天数,默认 7)
|
||||||
|
- `is_ladder`: 是否阶梯分佣(BOOLEAN,默认 false)
|
||||||
|
- `status`: 规则状态(INT,1-有效 2-无效)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
**字段使用规则**:
|
||||||
|
- **一次性分佣**: 使用 `series_id` 关联套餐系列,`package_id` 为 NULL
|
||||||
|
- **长期分佣**: 使用 `package_id` 关联具体套餐,`series_id` 为 NULL
|
||||||
|
- **组合分佣**: 需要创建两条规则记录,一条一次性(使用 `series_id`),一条长期(使用 `package_id`)
|
||||||
|
- **`series_id` 和 `package_id` 互斥**: 不能同时有值
|
||||||
|
|
||||||
|
#### Scenario: 配置一次性分佣规则
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)配置一次性分佣规则,套餐系列 ID 为 1(月套餐系列),固定金额 5.00 元
|
||||||
|
- **THEN** 系统创建分佣规则,`agent_id` 为 123,`commission_type` 为 "one_time",`series_id` 为 1,`package_id` 为 NULL,`commission_mode` 为 "fixed",`commission_value` 为 5.00
|
||||||
|
|
||||||
|
#### Scenario: 配置长期分佣规则
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)配置长期分佣规则,套餐 ID 为 3001,百分比 5%
|
||||||
|
- **THEN** 系统创建分佣规则,`agent_id` 为 123,`commission_type` 为 "long_term",`series_id` 为 NULL,`package_id` 为 3001,`commission_mode` 为 "percent",`commission_value` 为 0.05
|
||||||
|
|
||||||
|
#### Scenario: 配置组合分佣规则
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)配置组合分佣规则,套餐系列 ID 为 1,先一次性分佣 10.00 元,连续在网 3 个月后开始长期分佣(套餐 ID 为 3001)3.00 元/月
|
||||||
|
- **THEN** 系统创建两条分佣规则:
|
||||||
|
- 一条 `commission_type` 为 "one_time",`series_id` 为 1,`package_id` 为 NULL
|
||||||
|
- 另一条 `commission_type` 为 "long_term",`series_id` 为 NULL,`package_id` 为 3001,且关联组合条件
|
||||||
|
|
||||||
|
#### Scenario: 字段互斥校验
|
||||||
|
|
||||||
|
- **WHEN** 平台尝试创建分佣规则,同时设置 `series_id` 为 1 和 `package_id` 为 3001
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"`series_id` 和 `package_id` 不能同时有值"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 组合分佣条件配置
|
||||||
|
|
||||||
|
系统 SHALL 支持为组合分佣配置解冻条件,包括时间点条件和套餐周期条件。
|
||||||
|
|
||||||
|
**commission_combined_conditions 表**:
|
||||||
|
- `id`: 组合条件 ID(主键,BIGINT)
|
||||||
|
- `commission_rule_id`: 关联的分佣规则 ID(BIGINT,必须是 commission_type 为 "long_term" 且属于组合分佣的规则)
|
||||||
|
- `condition_type`: 条件类型(VARCHAR(20),"time_point"-时间点 | "package_cycle"-套餐周期)
|
||||||
|
- `time_months`: 时间月数(INT,可空,仅当 condition_type 为 "time_point" 时有值,表示实名后多少个月)
|
||||||
|
- `package_cycle_threshold`: 套餐周期阈值(INT,可空,仅当 condition_type 为 "package_cycle" 时有值,表示使用多少个套餐周期)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
**解冻逻辑**: 组合分佣的长期部分,当满足**任一条件**(OR 关系)时开始产生长期分佣。
|
||||||
|
|
||||||
|
#### Scenario: 配置时间点条件
|
||||||
|
|
||||||
|
- **WHEN** 平台为组合分佣规则(ID 为 501)配置时间点条件,实名后 3 个月开始长期分佣
|
||||||
|
- **THEN** 系统创建组合条件记录,`commission_rule_id` 为 501,`condition_type` 为 "time_point",`time_months` 为 3
|
||||||
|
|
||||||
|
#### Scenario: 配置套餐周期条件
|
||||||
|
|
||||||
|
- **WHEN** 平台为组合分佣规则(ID 为 501)配置套餐周期条件,使用 10 个套餐周期后开始长期分佣
|
||||||
|
- **THEN** 系统创建组合条件记录,`commission_rule_id` 为 501,`condition_type` 为 "package_cycle",`package_cycle_threshold` 为 10
|
||||||
|
|
||||||
|
#### Scenario: 同时配置两种条件(OR 关系)
|
||||||
|
|
||||||
|
- **WHEN** 平台为组合分佣规则(ID 为 501)同时配置时间点条件(6 个月)和套餐周期条件(10 个周期)
|
||||||
|
- **THEN** 系统创建两条组合条件记录,长期分佣在任一条件满足时开始
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 阶梯分佣配置
|
||||||
|
|
||||||
|
系统 SHALL 支持阶梯分佣,根据激活量/提货量达到阶梯条件后变更分佣值。
|
||||||
|
|
||||||
|
**commission_ladder 表**:
|
||||||
|
- `id`: 阶梯配置 ID(主键,BIGINT)
|
||||||
|
- `commission_rule_id`: 关联的分佣规则 ID(BIGINT)
|
||||||
|
- `ladder_type`: 阶梯类型(VARCHAR(20),"activation"-激活量 | "pickup"-提货量 | "deposit"-保证金)
|
||||||
|
- `ladder_threshold`: 阶梯阈值(INT,如激活 100 张)
|
||||||
|
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed"-固定金额 | "percent"-百分比)
|
||||||
|
- `commission_value`: 分佣值(DECIMAL(10,4),达到阶梯后的分佣值)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 配置激活量阶梯
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)配置阶梯分佣,激活 100 张卡后分佣从 5.00 元提升到 8.00 元
|
||||||
|
- **THEN** 系统创建阶梯配置,`ladder_type` 为 "activation",`ladder_threshold` 为 100,`commission_value` 为 8.00
|
||||||
|
|
||||||
|
#### Scenario: 计算阶梯分佣
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)当月激活量达到 100 张
|
||||||
|
- **THEN** 系统根据阶梯配置,从第 101 张卡开始使用新的分佣值 8.00 元
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣记录管理
|
||||||
|
|
||||||
|
系统 SHALL 记录每笔分佣,支持冻结、解冻和发放流程。
|
||||||
|
|
||||||
|
**commission_records 表**:
|
||||||
|
- `id`: 分佣记录 ID(主键,BIGINT)
|
||||||
|
- `agent_id`: 代理用户 ID(BIGINT)
|
||||||
|
- `order_id`: 订单 ID(BIGINT)
|
||||||
|
- `commission_rule_id`: 分佣规则 ID(BIGINT)
|
||||||
|
- `commission_type`: 分佣类型(VARCHAR(20),"one_time" | "long_term" | "combined")
|
||||||
|
- `amount`: 分佣金额(DECIMAL(10,2),元)
|
||||||
|
- `status`: 分佣状态(INT,1-冻结 2-解冻中 3-已发放 4-已失效)
|
||||||
|
- `freeze_until`: 冻结截止时间(TIMESTAMP,可空)
|
||||||
|
- `released_at`: 发放时间(TIMESTAMP,可空)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建一次性分佣记录
|
||||||
|
|
||||||
|
- **WHEN** 订单(ID 为 10001)完成,触发代理(ID 为 123)的一次性分佣 5.00 元,冻结 7 天
|
||||||
|
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结),`freeze_until` 为 7 天后
|
||||||
|
|
||||||
|
#### Scenario: 分佣自动解冻
|
||||||
|
|
||||||
|
- **WHEN** 分佣记录(ID 为 1001)的冻结截止时间到达,且满足解冻条件(激活+实名+充值)
|
||||||
|
- **THEN** 系统将分佣状态从 1(冻结) 变更为 2(解冻中),创建分佣解冻审批记录
|
||||||
|
|
||||||
|
#### Scenario: 分佣发放
|
||||||
|
|
||||||
|
- **WHEN** 分佣解冻审批通过
|
||||||
|
- **THEN** 系统将分佣状态从 2(解冻中) 变更为 3(已发放),将分佣金额转入代理钱包,`released_at` 记录发放时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣解冻条件
|
||||||
|
|
||||||
|
系统 SHALL 根据分佣类型校验不同的解冻条件。
|
||||||
|
|
||||||
|
**一次性分佣解冻条件**:
|
||||||
|
- 激活(实名状态为已实名;对于行业卡,实名状态可以为未实名)
|
||||||
|
- 达到累计/首次充值金额
|
||||||
|
- 冻结天数到达
|
||||||
|
|
||||||
|
**长期分佣解冻条件**:
|
||||||
|
- 激活(实名状态为已实名;对于行业卡,实名状态可以为未实名)
|
||||||
|
- 达到累计/首次充值金额
|
||||||
|
- 在网状态正常
|
||||||
|
- 三无校验通过(通过 Excel 导入解冻)
|
||||||
|
|
||||||
|
**组合分佣解冻条件**:
|
||||||
|
- **一次性部分**: 立即产生并按一次性分佣条件解冻
|
||||||
|
- **长期部分**: 当满足以下**任一条件**时开始长期分佣(OR 关系):
|
||||||
|
- 达到某个时间点之后(例如:实名后 3 个月)
|
||||||
|
- **OR** 该 IoT 卡的套餐使用周期数达到阈值(例如:10 个周期)
|
||||||
|
- **注意**: 套餐周期阈值是针对单张 IoT 卡的,不是设备级别
|
||||||
|
|
||||||
|
#### Scenario: 一次性分佣满足解冻条件
|
||||||
|
|
||||||
|
- **WHEN** 分佣记录(ID 为 1001)的冻结截止时间到达,用户已实名且已充值
|
||||||
|
- **THEN** 系统将分佣状态变更为 2(解冻中),创建审批记录
|
||||||
|
|
||||||
|
#### Scenario: 长期分佣等待 Excel 导入解冻
|
||||||
|
|
||||||
|
- **WHEN** 长期分佣记录等待三无校验
|
||||||
|
- **THEN** 系统保持分佣状态为 1(冻结),等待平台通过 Excel 导入解冻数据
|
||||||
|
|
||||||
|
#### Scenario: 组合分佣时间点条件满足
|
||||||
|
|
||||||
|
- **WHEN** 组合分佣规则配置为实名后 3 个月开始长期分佣,IoT 卡已实名 3 个月
|
||||||
|
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录,即使套餐周期数未达到阈值
|
||||||
|
|
||||||
|
#### Scenario: 组合分佣套餐周期条件满足
|
||||||
|
|
||||||
|
- **WHEN** 组合分佣规则配置为套餐使用 10 个周期后开始长期分佣,IoT 卡已使用套餐 10 个周期
|
||||||
|
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录,即使未达到时间点要求
|
||||||
|
|
||||||
|
#### Scenario: 组合分佣任一条件满足即开始
|
||||||
|
|
||||||
|
- **WHEN** 组合分佣规则配置为"实名后 6 个月 OR 10 个套餐周期",IoT 卡已使用 10 个周期但只实名 2 个月
|
||||||
|
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录(因为套餐周期条件已满足)
|
||||||
|
|
||||||
|
#### Scenario: 行业卡一次性分佣解冻(无需实名)
|
||||||
|
|
||||||
|
- **WHEN** 行业卡(card_category 为 "industry")的一次性分佣记录冻结期到达,卡已激活且已充值,但实名状态为未实名
|
||||||
|
- **THEN** 系统判定解冻条件满足(行业卡无需实名认证),将分佣状态变更为 2(解冻中),创建审批记录
|
||||||
|
|
||||||
|
#### Scenario: 行业卡长期分佣解冻(无需实名)
|
||||||
|
|
||||||
|
- **WHEN** 行业卡(card_category 为 "industry")的长期分佣记录满足充值金额和在网状态,但实名状态为未实名
|
||||||
|
- **THEN** 系统判定行业卡无需实名认证,等待三无校验通过后可解冻
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣解冻审批
|
||||||
|
|
||||||
|
系统 SHALL 支持分佣解冻审批流程,审批通过后发放分佣。
|
||||||
|
|
||||||
|
**commission_approvals 表**:
|
||||||
|
- `id`: 审批记录 ID(主键,BIGINT)
|
||||||
|
- `commission_record_id`: 分佣记录 ID(BIGINT)
|
||||||
|
- `approval_type`: 审批类型(VARCHAR(20),"auto"-自动 | "manual"-人工)
|
||||||
|
- `status`: 审批状态(INT,1-待审批 2-已通过 3-已拒绝)
|
||||||
|
- `approver_id`: 审批人用户 ID(BIGINT,可空)
|
||||||
|
- `approval_time`: 审批时间(TIMESTAMP,可空)
|
||||||
|
- `approval_note`: 审批备注(TEXT,可空)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建审批记录
|
||||||
|
|
||||||
|
- **WHEN** 分佣记录(ID 为 1001)状态变更为 2(解冻中)
|
||||||
|
- **THEN** 系统创建审批记录,`commission_record_id` 为 1001,`approval_type` 为 "auto",状态为 1(待审批)
|
||||||
|
|
||||||
|
#### Scenario: 审批通过
|
||||||
|
|
||||||
|
- **WHEN** 审批人(用户 ID 为 999)审批通过审批记录(ID 为 2001)
|
||||||
|
- **THEN** 系统将审批状态变更为 2(已通过),分佣记录状态变更为 3(已发放),将分佣金额转入代理钱包
|
||||||
|
|
||||||
|
#### Scenario: 审批拒绝
|
||||||
|
|
||||||
|
- **WHEN** 审批人拒绝审批记录(ID 为 2001),备注"用户未满足在网条件"
|
||||||
|
- **THEN** 系统将审批状态变更为 3(已拒绝),分佣记录状态变更为 4(已失效)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣模板
|
||||||
|
|
||||||
|
系统 SHALL 支持创建分佣模板,存储常用的分佣方案,便于快速配置。
|
||||||
|
|
||||||
|
**commission_templates 表**:
|
||||||
|
- `id`: 模板 ID(主键,BIGINT)
|
||||||
|
- `template_name`: 模板名称(VARCHAR(255))
|
||||||
|
- `business_type`: 业务类型(VARCHAR(20),"iot_card"-IoT卡 | "number_card"-号卡)
|
||||||
|
- `commission_type`: 分佣类型(VARCHAR(20),"one_time" | "long_term" | "combined")
|
||||||
|
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed" | "percent")
|
||||||
|
- `commission_value`: 分佣值(DECIMAL(10,4))
|
||||||
|
- `freeze_days`: 冻结天数(INT)
|
||||||
|
- `is_ladder`: 是否阶梯分佣(BOOLEAN)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建分佣模板
|
||||||
|
|
||||||
|
- **WHEN** 平台创建分佣模板"标准月套餐分佣",业务类型为 IoT 卡,一次性分佣 5.00 元,冻结 7 天
|
||||||
|
- **THEN** 系统创建模板记录,`template_name` 为 "标准月套餐分佣",`business_type` 为 "iot_card",`commission_type` 为 "one_time",`commission_value` 为 5.00,`freeze_days` 为 7
|
||||||
|
|
||||||
|
#### Scenario: 应用分佣模板
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)应用模板(ID 为 501)
|
||||||
|
- **THEN** 系统根据模板配置创建分佣规则,`agent_id` 为 123,其他字段从模板复制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 多级代理分佣
|
||||||
|
|
||||||
|
系统 SHALL 支持多级代理分佣,根据代理路径计算每一级代理的分佣。
|
||||||
|
|
||||||
|
**多级分佣规则**:
|
||||||
|
- 通过代理路径(`path`)获取整个代理链
|
||||||
|
- 为每一级代理查找对应的分佣规则
|
||||||
|
- 创建多条分佣记录,每条对应一个代理
|
||||||
|
|
||||||
|
#### Scenario: 三级代理分佣
|
||||||
|
|
||||||
|
- **WHEN** 订单(ID 为 10001)的代理路径为 "101/102/103",每级代理配置分佣:101(2.00 元)、102(3.00 元)、103(5.00 元)
|
||||||
|
- **THEN** 系统创建 3 条分佣记录:代理 101 的 2.00 元、代理 102 的 3.00 元、代理 103 的 5.00 元
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对分佣数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 代理 ID(agent_id):必填,≥ 1
|
||||||
|
- 订单 ID(order_id):必填,≥ 1
|
||||||
|
- 分佣金额(amount):必填,≥ 0,最多 2 位小数
|
||||||
|
- 分佣状态(status):必填,枚举值 1-4
|
||||||
|
- 冻结天数(freeze_days):必填,≥ 0
|
||||||
|
|
||||||
|
#### Scenario: 创建分佣记录时金额为负数
|
||||||
|
|
||||||
|
- **WHEN** 创建分佣记录,金额为 -5.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"分佣金额必须 ≥ 0"
|
||||||
|
|
||||||
|
#### Scenario: 创建分佣规则时分佣值无效
|
||||||
|
|
||||||
|
- **WHEN** 创建分佣规则,分佣模式为百分比,分佣值为 1.5(超过 100%)
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"百分比分佣值必须在 0-1 之间"
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: IoT 卡实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义 IoT 卡(IotCard)实体,包含 IoT 卡(物联网卡/流量卡/SIM卡)的商品属性、状态属性、所有权信息和 Gateway 集成字段。
|
||||||
|
|
||||||
|
**核心概念**: IoT 卡 = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)。系统使用 ICCID 作为 IoT 卡的唯一标识。
|
||||||
|
|
||||||
|
**卡业务类型**:
|
||||||
|
- **普通卡(normal)**: 需要实名认证才能激活使用,遵循运营商实名制要求
|
||||||
|
- **行业卡(industry)**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
|
||||||
|
**商品属性**:
|
||||||
|
- `id`: IoT 卡 ID(主键,BIGINT)
|
||||||
|
- `iccid`: ICCID(VARCHAR(50),唯一,国际移动用户识别码,IoT卡的唯一标识)
|
||||||
|
- `card_type`: 卡类型(VARCHAR(50),如 "4G"、"5G"、"NB-IoT")
|
||||||
|
- `card_category`: 卡业务类型(VARCHAR(20),枚举值:"normal"-普通卡 | "industry"-行业卡,默认 "normal")
|
||||||
|
- `carrier_id`: 运营商 ID(BIGINT,关联 carriers 表,如中国移动、中国联通、中国电信)
|
||||||
|
- `imsi`: IMSI(VARCHAR(50),可选,国际移动用户识别码)
|
||||||
|
- `msisdn`: 手机号码(VARCHAR(20),可选)
|
||||||
|
- `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯)
|
||||||
|
- `supplier`: 供应商名称(VARCHAR(255),可选)
|
||||||
|
- `cost_price`: 成本价(DECIMAL(10,2),平台进货价)
|
||||||
|
- `distribute_price`: 分销价(DECIMAL(10,2),分销给代理的价格,仅当 owner_type 为 agent 时有值)
|
||||||
|
|
||||||
|
**所有权和状态**:
|
||||||
|
- `status`: IoT 卡状态(INT,1-在库 2-已分销 3-已激活 4-已停用)
|
||||||
|
- `owner_type`: 所有者类型(VARCHAR(20),"platform"-平台自营 | "agent"-代理商 | "user"-用户 | "device"-设备)
|
||||||
|
- `owner_id`: 所有者 ID(BIGINT,platform 时为 0,agent/user/device 时为对应的 ID)
|
||||||
|
- `activated_at`: 激活时间(TIMESTAMP,可空)
|
||||||
|
|
||||||
|
**Gateway 集成字段**(从 Gateway 项目同步):
|
||||||
|
- `activation_status`: 激活状态(INT,0-未激活 1-已激活)
|
||||||
|
- `real_name_status`: 实名状态(INT,0-未实名 1-已实名)
|
||||||
|
- `network_status`: 网络状态(INT,0-停机 1-开机)
|
||||||
|
- `data_usage_mb`: 累计流量使用(BIGINT,MB 为单位,默认 0)
|
||||||
|
- `last_sync_time`: 最后一次与 Gateway 同步时间(TIMESTAMP,可空)
|
||||||
|
|
||||||
|
**轮询控制字段**:
|
||||||
|
- `enable_polling`: 是否参与轮询(BOOLEAN,默认 true,用于控制是否对该卡进行定时轮询)
|
||||||
|
- `last_data_check_at`: 最后一次卡流量检查时间(TIMESTAMP,可空,记录上次轮询卡流量的时间)
|
||||||
|
- `last_real_name_check_at`: 最后一次实名检查时间(TIMESTAMP,可空,记录上次轮询实名状态的时间)
|
||||||
|
|
||||||
|
**系统字段**:
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建平台自营 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 平台批量导入 IoT 卡数据,ICCID 为 "89860123456789012345"
|
||||||
|
- **THEN** 系统创建 IoT 卡记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(在库),`activation_status` 为 0(未激活)
|
||||||
|
|
||||||
|
#### Scenario: 平台分销 IoT 卡给代理
|
||||||
|
|
||||||
|
- **WHEN** 平台将在库 IoT 卡分销给代理商(用户 ID 为 123),设置分销价为 50.00 元
|
||||||
|
- **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 2(已分销),`owner_type` 变更为 "agent",`owner_id` 设置为 123,`distribute_price` 设置为 50.00
|
||||||
|
|
||||||
|
#### Scenario: IoT 卡绑定到设备
|
||||||
|
|
||||||
|
- **WHEN** 用户将 IoT 卡(ICCID 为 "8986...")绑定到设备(ID 为 1001)
|
||||||
|
- **THEN** 系统在 `device_sim_bindings` 表创建绑定记录,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为 1001
|
||||||
|
|
||||||
|
#### Scenario: IoT 卡直接销售给用户
|
||||||
|
|
||||||
|
- **WHEN** 平台或代理将 IoT 卡直接销售给用户(用户 ID 为 2001)
|
||||||
|
- **THEN** 系统创建套餐订单记录,IoT 卡的 `owner_type` 变更为 "user",`owner_id` 变更为 2001
|
||||||
|
|
||||||
|
#### Scenario: 行业卡无需实名认证
|
||||||
|
|
||||||
|
- **WHEN** 创建卡业务类型为 "industry"(行业卡)的 IoT 卡
|
||||||
|
- **THEN** 系统允许该卡在 `real_name_status` 为 0(未实名)的情况下激活使用,不强制要求实名认证
|
||||||
|
|
||||||
|
#### Scenario: 普通卡需要实名认证
|
||||||
|
|
||||||
|
- **WHEN** 创建卡业务类型为 "normal"(普通卡)的 IoT 卡
|
||||||
|
- **THEN** 系统要求该卡必须先完成实名认证(`real_name_status` 为 1)才能激活使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡状态流转
|
||||||
|
|
||||||
|
系统 SHALL 管理 IoT 卡的状态流转,确保状态变更符合业务规则。
|
||||||
|
|
||||||
|
**状态定义**:
|
||||||
|
- **1-在库**: IoT 卡在平台库存中,未分销
|
||||||
|
- **2-已分销**: IoT 卡已分销给代理商,代理可销售
|
||||||
|
- **3-已激活**: IoT 卡已被终端用户激活使用
|
||||||
|
- **4-已停用**: IoT 卡已停用,不可使用
|
||||||
|
|
||||||
|
**状态流转规则**:
|
||||||
|
- 在库(1) → 已分销(2): 平台分销给代理
|
||||||
|
- 在库(1) → 已激活(3): 平台自营直接销售给用户并激活
|
||||||
|
- 已分销(2) → 已激活(3): 代理销售给用户并激活
|
||||||
|
- 已激活(3) → 已停用(4): 用户或平台主动停用
|
||||||
|
- 已停用(4) → 已激活(3): 用户或平台主动复机(仅在符合业务规则时)
|
||||||
|
|
||||||
|
#### Scenario: 代理销售 IoT 卡给用户
|
||||||
|
|
||||||
|
- **WHEN** 代理商销售已分销 IoT 卡给终端用户并激活
|
||||||
|
- **THEN** 系统将 IoT 卡状态从 2(已分销) 变更为 3(已激活),`activated_at` 记录激活时间,`activation_status` 从 Gateway 同步后变更为 1
|
||||||
|
|
||||||
|
#### Scenario: 平台自营销售 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 平台直接销售在库 IoT 卡给终端用户并激活
|
||||||
|
- **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 3(已激活),`owner_type` 保持 "platform",`activated_at` 记录激活时间
|
||||||
|
|
||||||
|
#### Scenario: 停用已激活 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 用户或平台停用已激活 IoT 卡
|
||||||
|
- **THEN** 系统将 IoT 卡状态从 3(已激活) 变更为 4(已停用),通过 Gateway API 执行停机操作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡平台自营和代理分销
|
||||||
|
|
||||||
|
系统 SHALL 支持 IoT 卡的平台自营销售和代理分销两种模式,通过 `owner_type` 和 `owner_id` 区分所有者。
|
||||||
|
|
||||||
|
**平台自营**:
|
||||||
|
- `owner_type` 为 "platform"
|
||||||
|
- `owner_id` 为 0
|
||||||
|
- 平台直接销售给终端用户
|
||||||
|
- 销售价格由平台自主定价
|
||||||
|
|
||||||
|
**代理分销**:
|
||||||
|
- `owner_type` 为 "agent"
|
||||||
|
- `owner_id` 为代理用户 ID
|
||||||
|
- 代理商可以销售给终端用户或下级代理
|
||||||
|
- 分销价格由平台设置(`distribute_price`),代理商可在分销价基础上加价(但不能超过 2 倍)
|
||||||
|
|
||||||
|
#### Scenario: 查询平台自营 IoT 卡库存
|
||||||
|
|
||||||
|
- **WHEN** 查询平台自营 IoT 卡库存
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "platform" 且 `status` 为 1(在库) 的 IoT 卡列表
|
||||||
|
|
||||||
|
#### Scenario: 查询代理分销 IoT 卡库存
|
||||||
|
|
||||||
|
- **WHEN** 代理商(用户 ID 为 123)查询自己的 IoT 卡库存
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 且 `status` 为 2(已分销) 的 IoT 卡列表
|
||||||
|
|
||||||
|
#### Scenario: 代理加价销售 IoT 卡套餐
|
||||||
|
|
||||||
|
- **WHEN** 代理商为已分销 IoT 卡设置套餐售价
|
||||||
|
- **THEN** 系统校验套餐售价不超过分销价的 2 倍,校验通过后允许销售
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡批量导入
|
||||||
|
|
||||||
|
系统 SHALL 支持批量导入 IoT 卡数据,用于初始化库存或补充库存。
|
||||||
|
|
||||||
|
**导入字段**:
|
||||||
|
- ICCID(必填)
|
||||||
|
- 卡类型(必填,如 "4G"、"5G"、"NB-IoT")
|
||||||
|
- 卡业务类型(可选,枚举值 "normal" | "industry",默认 "normal")
|
||||||
|
- 运营商 ID(必填,从 carriers 表中选择)
|
||||||
|
- IMSI(可选)
|
||||||
|
- 手机号码(可选)
|
||||||
|
- 供应商(可选)
|
||||||
|
- 成本价(必填)
|
||||||
|
- 批次号(必填)
|
||||||
|
|
||||||
|
**导入规则**:
|
||||||
|
- ICCID 必须唯一,重复 ICCID 将被拒绝
|
||||||
|
- 导入的 IoT 卡默认状态为 1(在库),所有者为平台(`owner_type` 为 "platform",`owner_id` 为 0)
|
||||||
|
- 导入成功后记录操作日志
|
||||||
|
|
||||||
|
#### Scenario: 批量导入 IoT 卡成功
|
||||||
|
|
||||||
|
- **WHEN** 平台上传包含 100 条 IoT 卡数据的 CSV 文件
|
||||||
|
- **THEN** 系统创建 100 条 IoT 卡记录,状态为 1(在库),所有者为平台,返回导入成功消息
|
||||||
|
|
||||||
|
#### Scenario: 批量导入包含重复 ICCID
|
||||||
|
|
||||||
|
- **WHEN** 平台上传的 CSV 文件中包含已存在的 ICCID
|
||||||
|
- **THEN** 系统拒绝重复 ICCID 的 IoT 卡,返回错误信息并列出重复 ICCID,其他有效 IoT 卡正常导入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡查询和筛选
|
||||||
|
|
||||||
|
系统 SHALL 支持多维度查询和筛选 IoT 卡,包括状态、所有者、批次号、卡类型等。
|
||||||
|
|
||||||
|
**查询条件**:
|
||||||
|
- ICCID(精确匹配或模糊匹配)
|
||||||
|
- IoT 卡状态(单选或多选)
|
||||||
|
- 所有者类型(platform | agent | user | device)
|
||||||
|
- 所有者 ID(仅当所有者类型为 agent/user/device 时有效)
|
||||||
|
- 批次号(精确匹配)
|
||||||
|
- 卡类型(单选或多选)
|
||||||
|
- 运营商 ID(单选或多选,从 carriers 表选择)
|
||||||
|
- 激活状态(0-未激活 | 1-已激活)
|
||||||
|
- 实名状态(0-未实名 | 1-已实名)
|
||||||
|
- 网络状态(0-停机 | 1-开机)
|
||||||
|
- 是否参与轮询(true | false)
|
||||||
|
- 激活时间范围(开始时间 - 结束时间)
|
||||||
|
- 创建时间范围(开始时间 - 结束时间)
|
||||||
|
|
||||||
|
**分页**:
|
||||||
|
- 默认每页 20 条,最大每页 100 条
|
||||||
|
- 返回总记录数和总页数
|
||||||
|
|
||||||
|
#### Scenario: 查询特定批次的在库 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 平台查询批次号为 "BATCH-2025-001" 且状态为 1(在库) 的 IoT 卡
|
||||||
|
- **THEN** 系统返回符合条件的 IoT 卡列表,包含 ICCID、类型、运营商、成本价等信息
|
||||||
|
|
||||||
|
#### Scenario: 代理查询自己的已分销 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 代理商(用户 ID 为 123)查询自己的已分销 IoT 卡
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 且 `status` 为 2(已分销) 的 IoT 卡列表
|
||||||
|
|
||||||
|
#### Scenario: 分页查询 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 平台查询在库 IoT 卡,指定每页 50 条,查询第 2 页
|
||||||
|
- **THEN** 系统返回第 51-100 条 IoT 卡记录,以及总记录数和总页数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Gateway 集成
|
||||||
|
|
||||||
|
系统 SHALL 预留 IoT 卡状态相关字段,用于后续与 Gateway 项目集成。
|
||||||
|
|
||||||
|
**集成字段**:
|
||||||
|
- `activation_status`: 激活状态(从 Gateway 同步)
|
||||||
|
- `real_name_status`: 实名状态(从 Gateway 同步)
|
||||||
|
- `network_status`: 网络状态(从 Gateway 同步)
|
||||||
|
- `data_usage_mb`: 累计流量使用(从 Gateway 同步)
|
||||||
|
- `last_sync_time`: 最后同步时间
|
||||||
|
|
||||||
|
**集成说明**:
|
||||||
|
- 本阶段只设计数据模型字段,不实现 Gateway HTTP 客户端代码
|
||||||
|
- 后续 Service 层将调用 Gateway API 获取 IoT 卡状态并更新这些字段
|
||||||
|
- Gateway 使用 AES 加密 + MD5 签名的统一传输协议(参考 design.md)
|
||||||
|
|
||||||
|
**Gateway API 功能**:
|
||||||
|
- 查询 IoT 卡状态(激活状态、实名状态、网络状态)
|
||||||
|
- 查询流量详情(累计流量使用、剩余流量)
|
||||||
|
- 停复机操作(停机、复机)
|
||||||
|
- 实名认证操作
|
||||||
|
|
||||||
|
#### Scenario: 预留 Gateway 集成字段
|
||||||
|
|
||||||
|
- **WHEN** 创建 IoT 卡记录
|
||||||
|
- **THEN** 系统初始化 Gateway 相关字段为默认值:`activation_status` 为 0,`real_name_status` 为 0,`network_status` 为 0,`data_usage_mb` 为 0,`last_sync_time` 为空
|
||||||
|
|
||||||
|
#### Scenario: 从 Gateway 同步 IoT 卡状态
|
||||||
|
|
||||||
|
- **WHEN** Service 层调用 Gateway API 查询 IoT 卡状态
|
||||||
|
- **THEN** 系统更新 IoT 卡的 `activation_status`、`real_name_status`、`network_status`、`data_usage_mb` 和 `last_sync_time` 字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对 IoT 卡数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- ICCID(iccid):必填,长度 19-20 字符,唯一
|
||||||
|
- 卡类型(card_type):必填,长度 1-50 字符
|
||||||
|
- 卡业务类型(card_category):必填,枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
|
||||||
|
- 运营商 ID(carrier_id):必填,≥ 1,必须是有效的运营商 ID
|
||||||
|
- 成本价(cost_price):必填,≥ 0,最多 2 位小数
|
||||||
|
- 分销价(distribute_price):可选,≥ 0,最多 2 位小数,≥ 成本价
|
||||||
|
- 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user" | "device"
|
||||||
|
- 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
|
||||||
|
- 激活状态(activation_status):必填,枚举值 0(未激活) | 1(已激活)
|
||||||
|
- 实名状态(real_name_status):必填,枚举值 0(未实名) | 1(已实名),当 card_category 为 "industry"(行业卡)时可以保持 0
|
||||||
|
- 网络状态(network_status):必填,枚举值 0(停机) | 1(开机)
|
||||||
|
- 轮询开关(enable_polling):必填,布尔值 true | false
|
||||||
|
|
||||||
|
#### Scenario: 创建 IoT 卡时 ICCID 格式错误
|
||||||
|
|
||||||
|
- **WHEN** 平台创建 IoT 卡,ICCID 长度为 15(小于 19)
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"ICCID 长度必须为 19-20 字符"
|
||||||
|
|
||||||
|
#### Scenario: 创建 IoT 卡时 ICCID 重复
|
||||||
|
|
||||||
|
- **WHEN** 平台创建 IoT 卡,ICCID 为已存在的 "89860123456789012345"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"ICCID 已存在"
|
||||||
|
|
||||||
|
#### Scenario: 创建 IoT 卡时成本价为负数
|
||||||
|
|
||||||
|
- **WHEN** 平台创建 IoT 卡,成本价为 -10.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"成本价必须 ≥ 0"
|
||||||
|
|
||||||
|
#### Scenario: 创建 IoT 卡时分销价低于成本价
|
||||||
|
|
||||||
|
- **WHEN** 平台创建 IoT 卡,成本价为 50.00,分销价为 40.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"分销价不能低于成本价"
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 设备实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义设备(Device)实体,用于管理用户的物联网设备(如 GPS 追踪器、智能传感器等),支持设备与 IoT 卡的绑定关系、设备批量分配和设备操作。
|
||||||
|
|
||||||
|
**核心概念**: 设备不在卡管系统中销售,主要用于:
|
||||||
|
1. 用户设备管理(用户添加自己的设备,绑定 IoT 卡)
|
||||||
|
2. 方便运营人员管理投诉和代理要求(通过设备维度批量查看绑定的所有 IoT 卡)
|
||||||
|
3. 设备操作(重启、修改账号密码、重置等)
|
||||||
|
4. 设备批量分配(运营人员在别的系统报单后发货,把设备和绑定的 IoT 卡一起分配给代理)
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
|
||||||
|
**基本属性**:
|
||||||
|
- `id`: 设备 ID(主键,BIGINT)
|
||||||
|
- `device_no`: 设备编号(唯一,VARCHAR(50))
|
||||||
|
- `device_name`: 设备名称(VARCHAR(255))
|
||||||
|
- `device_model`: 设备型号(VARCHAR(100))
|
||||||
|
- `device_type`: 设备类型(VARCHAR(50),如 "GPS Tracker"、"Camera"、"Sensor")
|
||||||
|
- `max_sim_slots`: 最大 IoT 卡插槽数量(INT,1-4,默认 4)
|
||||||
|
- `manufacturer`: 设备制造商(VARCHAR(255),可选)
|
||||||
|
- `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯)
|
||||||
|
|
||||||
|
**所有权和状态**:
|
||||||
|
- `owner_type`: 所有者类型(VARCHAR(20),"platform"-平台库存(等待分配) | "agent"-代理商 | "user"-用户)
|
||||||
|
- `owner_id`: 所有者 ID(BIGINT,platform 时为 0,agent/user 时为对应的 ID)
|
||||||
|
- `status`: 设备状态(INT,1-未激活 2-已激活 3-已停用)
|
||||||
|
- `activated_at`: 激活时间(TIMESTAMP,可空)
|
||||||
|
|
||||||
|
**设备操作配置**(预留字段,用于后续设备操作功能):
|
||||||
|
- `device_username`: 设备登录账号(VARCHAR(100),可选)
|
||||||
|
- `device_password_encrypted`: 设备登录密码(加密存储,TEXT,可选)
|
||||||
|
- `device_api_endpoint`: 设备 API 接口地址(VARCHAR(500),可选)
|
||||||
|
|
||||||
|
**系统字段**:
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 用户添加设备
|
||||||
|
|
||||||
|
- **WHEN** 用户添加自己的设备(设备编号为 "GPS-001",设备名称为 "物流车辆追踪器")
|
||||||
|
- **THEN** 系统创建设备记录,`owner_type` 为 "user",`owner_id` 为用户 ID,状态为 1(未激活)
|
||||||
|
|
||||||
|
#### Scenario: 平台导入设备到库存
|
||||||
|
|
||||||
|
- **WHEN** 平台批量导入设备数据(准备发货给代理)
|
||||||
|
- **THEN** 系统创建设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
|
||||||
|
|
||||||
|
#### Scenario: 运营人员批量分配设备给代理
|
||||||
|
|
||||||
|
- **WHEN** 运营人员将平台库存设备(ID 为 1001)分配给代理商(用户 ID 为 123)
|
||||||
|
- **THEN** 系统将设备的 `owner_type` 变更为 "agent",`owner_id` 设置为 123,同时自动分配该设备绑定的所有 IoT 卡给代理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备状态流转
|
||||||
|
|
||||||
|
系统 SHALL 管理设备的状态流转,确保状态变更符合业务规则。
|
||||||
|
|
||||||
|
**状态定义**:
|
||||||
|
- **1-未激活**: 设备尚未激活使用
|
||||||
|
- **2-已激活**: 设备已被用户激活使用
|
||||||
|
- **3-已停用**: 设备已停用,不可使用
|
||||||
|
|
||||||
|
**状态流转规则**:
|
||||||
|
- 未激活(1) → 已激活(2): 用户激活设备
|
||||||
|
- 已激活(2) → 已停用(3): 用户或平台主动停用设备
|
||||||
|
- 已停用(3) → 已激活(2): 用户或平台主动恢复设备(仅在符合业务规则时)
|
||||||
|
|
||||||
|
#### Scenario: 用户激活设备
|
||||||
|
|
||||||
|
- **WHEN** 用户激活自己的设备
|
||||||
|
- **THEN** 系统将设备状态从 1(未激活) 变更为 2(已激活),`activated_at` 记录激活时间
|
||||||
|
|
||||||
|
#### Scenario: 用户停用设备
|
||||||
|
|
||||||
|
- **WHEN** 用户停用已激活的设备
|
||||||
|
- **THEN** 系统将设备状态从 2(已激活) 变更为 3(已停用),同时可选择是否停用该设备绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备与 IoT 卡绑定关系
|
||||||
|
|
||||||
|
系统 SHALL 管理设备与 IoT 卡的绑定关系,一个设备可以绑定 1-4 张 IoT 卡。
|
||||||
|
|
||||||
|
**绑定规则**:
|
||||||
|
- 一个设备最多绑定 4 张 IoT 卡(由 `max_sim_slots` 字段控制)
|
||||||
|
- 一个 IoT 卡同一时间只能绑定一个设备
|
||||||
|
- 绑定时记录插槽位置(slot_position: 1, 2, 3, 4)
|
||||||
|
- 绑定时记录绑定时间和绑定状态(1-已绑定 2-已解绑)
|
||||||
|
- 设备绑定 IoT 卡后,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为设备 ID
|
||||||
|
|
||||||
|
**中间表 device_sim_bindings**:
|
||||||
|
- `id`: 绑定记录 ID(主键,BIGINT)
|
||||||
|
- `device_id`: 设备 ID(BIGINT)
|
||||||
|
- `iot_card_id`: IoT 卡 ID(BIGINT)
|
||||||
|
- `slot_position`: 插槽位置(INT,1-4)
|
||||||
|
- `bind_status`: 绑定状态(INT,1-已绑定 2-已解绑)
|
||||||
|
- `bind_time`: 绑定时间(TIMESTAMP)
|
||||||
|
- `unbind_time`: 解绑时间(TIMESTAMP,可空)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 绑定 IoT 卡到设备
|
||||||
|
|
||||||
|
- **WHEN** 用户将 IoT 卡(ID 为 101)绑定到设备(ID 为 1001)的插槽 1
|
||||||
|
- **THEN** 系统创建绑定记录,`device_id` 为 1001,`iot_card_id` 为 101,`slot_position` 为 1,`bind_status` 为 1(已绑定),`bind_time` 为当前时间,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为 1001
|
||||||
|
|
||||||
|
#### Scenario: 绑定超过最大插槽数量
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试将第 5 张 IoT 卡绑定到最大插槽数为 4 的设备
|
||||||
|
- **THEN** 系统拒绝绑定,返回错误信息"设备插槽已满,最多支持 4 张 IoT 卡"
|
||||||
|
|
||||||
|
#### Scenario: 绑定已被占用的 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试绑定已被其他设备绑定的 IoT 卡
|
||||||
|
- **THEN** 系统拒绝绑定,返回错误信息"该 IoT 卡已被其他设备绑定"
|
||||||
|
|
||||||
|
#### Scenario: 解绑 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 用户解绑设备的 IoT 卡(绑定记录 ID 为 10)
|
||||||
|
- **THEN** 系统将绑定记录的 `bind_status` 从 1(已绑定) 变更为 2(已解绑),`unbind_time` 记录解绑时间,IoT 卡的 `owner_type` 和 `owner_id` 重置
|
||||||
|
|
||||||
|
#### Scenario: 查询设备当前绑定的 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 用户查询设备(ID 为 1001)当前绑定的 IoT 卡
|
||||||
|
- **THEN** 系统返回 `device_id` 为 1001 且 `bind_status` 为 1(已绑定) 的所有绑定记录,包含 IoT 卡信息(ICCID、运营商、激活状态等)和插槽位置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备套餐购买和流量共享
|
||||||
|
|
||||||
|
系统 SHALL 支持用户为设备购买套餐,套餐自动分配到设备绑定的所有 IoT 卡,流量在设备级别共享。
|
||||||
|
|
||||||
|
**设备套餐业务规则**:
|
||||||
|
- 用户为设备购买套餐时,套餐会分配到设备绑定的**所有 IoT 卡**(1-4 张)
|
||||||
|
- 套餐的流量是**设备级别共享的**(例如 3000G/月共享,不管用哪张卡)
|
||||||
|
- 分佣**只计算一次**(不按卡数倍增)
|
||||||
|
- 订单表通过 `device_id` 字段关联设备,通过 `device_sim_bindings` 表查找绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
**套餐分配示例**:
|
||||||
|
- 设备绑定 3 张 IoT 卡
|
||||||
|
- 用户购买套餐:399 元/年,每月 3000G 流量,长期佣金 100 元
|
||||||
|
- 用户支付:399 元
|
||||||
|
- 套餐分配:设备的 3 张 IoT 卡都获得该套餐
|
||||||
|
- 流量使用:3000G/月 在 3 张卡之间共享(不是每张卡 3000G,而是总共 3000G)
|
||||||
|
- 分佣:代理获得 100 元分佣(只分一次,不是 3 × 100 元)
|
||||||
|
|
||||||
|
#### Scenario: 用户为设备购买套餐
|
||||||
|
|
||||||
|
- **WHEN** 用户为设备(ID 为 1001,绑定 3 张 IoT 卡)购买套餐(套餐 ID 为 3001,399 元/年,3000G/月)
|
||||||
|
- **THEN** 系统创建套餐订单,`device_id` 为 1001,`package_id` 为 3001,订单金额为 399 元,将套餐分配到设备绑定的 3 张 IoT 卡,设置流量共享模式为设备级别
|
||||||
|
|
||||||
|
#### Scenario: 设备级流量共享
|
||||||
|
|
||||||
|
- **WHEN** 设备(ID 为 1001)的套餐流量为 3000G/月,设备绑定 3 张 IoT 卡
|
||||||
|
- **THEN** 系统设置流量共享模式,3 张 IoT 卡共享 3000G/月(不是每张卡 3000G),无论使用哪张卡,都从这个流量池扣除
|
||||||
|
|
||||||
|
#### Scenario: 设备套餐分佣
|
||||||
|
|
||||||
|
- **WHEN** 用户为设备购买套餐,订单金额为 399 元,代理的长期分佣规则为 100 元
|
||||||
|
- **THEN** 系统为代理创建一条分佣记录,分佣金额为 100 元(只分一次,不按设备绑定的卡数倍增)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备批量分配
|
||||||
|
|
||||||
|
系统 SHALL 支持运营人员批量分配设备给代理,设备分配时自动分配该设备绑定的所有 IoT 卡。
|
||||||
|
|
||||||
|
**分配规则**:
|
||||||
|
- 只能分配 `owner_type` 为 "platform" 的设备(平台库存)
|
||||||
|
- 分配时,设备的 `owner_type` 变更为 "agent",`owner_id` 设置为代理用户 ID
|
||||||
|
- 分配时,设备绑定的所有 IoT 卡的 `owner_type` 也变更为 "agent",`owner_id` 设置为代理用户 ID
|
||||||
|
- 分配操作记录到操作日志
|
||||||
|
|
||||||
|
#### Scenario: 运营人员批量分配设备
|
||||||
|
|
||||||
|
- **WHEN** 运营人员将 10 台设备(平台库存)分配给代理商(用户 ID 为 123)
|
||||||
|
- **THEN** 系统将这 10 台设备的 `owner_type` 变更为 "agent",`owner_id` 设置为 123,同时将这些设备绑定的所有 IoT 卡也分配给代理 123
|
||||||
|
|
||||||
|
#### Scenario: 分配已分配的设备
|
||||||
|
|
||||||
|
- **WHEN** 运营人员尝试分配 `owner_type` 为 "agent" 的设备
|
||||||
|
- **THEN** 系统拒绝分配,返回错误信息"该设备已分配给代理,不能重复分配"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备操作
|
||||||
|
|
||||||
|
系统 SHALL 支持对设备的远程操作(重启、修改账号密码、重置等),用于设备管理和故障排查。
|
||||||
|
|
||||||
|
**设备操作类型**:
|
||||||
|
- **重启设备**: 远程重启设备
|
||||||
|
- **修改账号密码**: 修改设备的登录账号和密码
|
||||||
|
- **重置设备**: 将设备恢复到出厂设置
|
||||||
|
- **查询设备状态**: 查询设备的在线状态、运行状态等
|
||||||
|
- **设备配置更新**: 更新设备的配置参数
|
||||||
|
|
||||||
|
**操作说明**:
|
||||||
|
- 本阶段只设计数据模型字段和接口定义,不实现设备操作的具体代码
|
||||||
|
- 后续 Service 层将调用设备厂商提供的 API 或通过 MQTT/HTTP 协议与设备通信
|
||||||
|
- 设备操作需要记录操作日志(操作类型、操作人、操作时间、操作结果)
|
||||||
|
|
||||||
|
#### Scenario: 重启设备
|
||||||
|
|
||||||
|
- **WHEN** 用户或运营人员请求重启设备(ID 为 1001)
|
||||||
|
- **THEN** 系统调用设备 API 发送重启命令,记录操作日志,返回操作结果
|
||||||
|
|
||||||
|
#### Scenario: 修改设备密码
|
||||||
|
|
||||||
|
- **WHEN** 用户或运营人员修改设备(ID 为 1001)的登录密码
|
||||||
|
- **THEN** 系统更新设备的 `device_password_encrypted` 字段(加密存储),调用设备 API 同步密码修改,记录操作日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备批量导入
|
||||||
|
|
||||||
|
系统 SHALL 支持批量导入设备数据,用于平台库存管理。
|
||||||
|
|
||||||
|
**导入字段**:
|
||||||
|
- 设备编号(必填)
|
||||||
|
- 设备名称(必填)
|
||||||
|
- 设备型号(必填)
|
||||||
|
- 设备类型(必填)
|
||||||
|
- 最大插槽数(可选,默认 4)
|
||||||
|
- 设备制造商(可选)
|
||||||
|
- 批次号(必填)
|
||||||
|
|
||||||
|
**导入规则**:
|
||||||
|
- 设备编号必须唯一,重复编号将被拒绝
|
||||||
|
- 导入的设备默认 `owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
|
||||||
|
- 导入成功后记录操作日志
|
||||||
|
|
||||||
|
#### Scenario: 批量导入设备成功
|
||||||
|
|
||||||
|
- **WHEN** 平台上传包含 50 条设备数据的 CSV 文件
|
||||||
|
- **THEN** 系统创建 50 条设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活),返回导入成功消息
|
||||||
|
|
||||||
|
#### Scenario: 批量导入包含重复编号
|
||||||
|
|
||||||
|
- **WHEN** 平台上传的 CSV 文件中包含已存在的设备编号
|
||||||
|
- **THEN** 系统拒绝重复编号的设备,返回错误信息并列出重复编号,其他有效设备正常导入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备查询和筛选
|
||||||
|
|
||||||
|
系统 SHALL 支持多维度查询和筛选设备,包括状态、所有者、批次号、设备类型等。
|
||||||
|
|
||||||
|
**查询条件**:
|
||||||
|
- 设备编号(精确匹配或模糊匹配)
|
||||||
|
- 设备名称(模糊匹配)
|
||||||
|
- 设备状态(单选或多选)
|
||||||
|
- 所有者类型(platform | agent | user)
|
||||||
|
- 所有者 ID(仅当所有者类型为 agent/user 时有效)
|
||||||
|
- 批次号(精确匹配)
|
||||||
|
- 设备类型(单选或多选)
|
||||||
|
- 设备制造商(模糊匹配)
|
||||||
|
- 激活时间范围(开始时间 - 结束时间)
|
||||||
|
- 创建时间范围(开始时间 - 结束时间)
|
||||||
|
|
||||||
|
**分页**:
|
||||||
|
- 默认每页 20 条,最大每页 100 条
|
||||||
|
- 返回总记录数和总页数
|
||||||
|
|
||||||
|
#### Scenario: 查询平台库存设备
|
||||||
|
|
||||||
|
- **WHEN** 运营人员查询平台库存设备
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "platform" 的设备列表
|
||||||
|
|
||||||
|
#### Scenario: 代理查询自己的设备
|
||||||
|
|
||||||
|
- **WHEN** 代理商(用户 ID 为 123)查询自己的设备
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 的设备列表
|
||||||
|
|
||||||
|
#### Scenario: 用户查询自己的设备
|
||||||
|
|
||||||
|
- **WHEN** 用户(用户 ID 为 2001)查询自己的设备
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "user" 且 `owner_id` 为 2001 的设备列表,包含设备绑定的所有 IoT 卡信息
|
||||||
|
|
||||||
|
#### Scenario: 运营人员通过设备查看绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 运营人员需要处理投诉,查询设备(ID 为 1001)绑定的所有 IoT 卡
|
||||||
|
- **THEN** 系统返回设备信息和绑定的所有 IoT 卡详细信息(ICCID、运营商、激活状态、流量使用等),方便统一查看和管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对设备数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 设备编号(device_no):必填,长度 1-50 字符,唯一
|
||||||
|
- 设备名称(device_name):必填,长度 1-255 字符
|
||||||
|
- 设备型号(device_model):必填,长度 1-100 字符
|
||||||
|
- 设备类型(device_type):必填,长度 1-50 字符
|
||||||
|
- 最大插槽数(max_sim_slots):必填,1-4 之间的整数
|
||||||
|
- 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user"
|
||||||
|
- 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
|
||||||
|
- 设备状态(status):必填,枚举值 1(未激活) | 2(已激活) | 3(已停用)
|
||||||
|
|
||||||
|
#### Scenario: 创建设备时插槽数超出范围
|
||||||
|
|
||||||
|
- **WHEN** 用户创建设备,最大插槽数为 5
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"最大插槽数必须在 1-4 之间"
|
||||||
|
|
||||||
|
#### Scenario: 创建设备时设备编号重复
|
||||||
|
|
||||||
|
- **WHEN** 用户创建设备,设备编号为已存在的 "DEV-001"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"设备编号已存在"
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 号卡实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义号卡(NumberCard)实体,作为运营商订单回传的映射,支持代理分销和分佣。
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
- `id`: 号卡 ID(主键,BIGINT)
|
||||||
|
- `virtual_product_code`: 虚拟商品编码(VARCHAR(100),唯一,用于对应运营商订单)
|
||||||
|
- `product_name`: 商品名称(VARCHAR(255))
|
||||||
|
- `carrier`: 运营商名称(VARCHAR(100),如 "中国移动"、"中国联通"、"中国电信")
|
||||||
|
- `carrier_product_id`: 运营商商品 ID(VARCHAR(100))
|
||||||
|
- `package_type`: 套餐类型(VARCHAR(50),如 "月套餐"、"流量包")
|
||||||
|
- `data_amount_mb`: 流量额度(BIGINT,MB 为单位,可选)
|
||||||
|
- `voice_minutes`: 语音分钟数(INT,可选)
|
||||||
|
- `sms_count`: 短信条数(INT,可选)
|
||||||
|
- `price`: 固定售价(DECIMAL(10,2),由运营商定价)
|
||||||
|
- `status`: 号卡状态(INT,1-上架 2-下架)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡商品
|
||||||
|
|
||||||
|
- **WHEN** 平台创建号卡商品,虚拟商品编码为 "VC-CMCC-001",运营商为"中国移动",固定售价为 30.00 元
|
||||||
|
- **THEN** 系统创建号卡记录,`virtual_product_code` 为 "VC-CMCC-001",`carrier` 为 "中国移动",`price` 为 30.00,状态为 1(上架)
|
||||||
|
|
||||||
|
#### Scenario: 虚拟商品编码唯一性
|
||||||
|
|
||||||
|
- **WHEN** 平台创建号卡商品,虚拟商品编码为已存在的 "VC-CMCC-001"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"虚拟商品编码已存在"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡运营商订单回传
|
||||||
|
|
||||||
|
系统 SHALL 接收 Gateway 项目转换后的运营商订单回传,通过虚拟商品编码匹配号卡,创建订单和分佣记录。
|
||||||
|
|
||||||
|
**订单回传字段**:
|
||||||
|
- `carrier_order_id`: 运营商订单 ID(VARCHAR(255),唯一)
|
||||||
|
- `virtual_product_code`: 虚拟商品编码(VARCHAR(100),用于匹配号卡)
|
||||||
|
- `user_phone`: 用户手机号(VARCHAR(20))
|
||||||
|
- `amount`: 订单金额(DECIMAL(10,2))
|
||||||
|
- `order_time`: 订单时间(TIMESTAMP)
|
||||||
|
- `agent_id`: 代理 ID(BIGINT,可空,如果通过代理推广则有值)
|
||||||
|
- `carrier_order_data`: 运营商订单原始数据(JSONB)
|
||||||
|
|
||||||
|
**回传处理流程**:
|
||||||
|
1. Gateway 接收运营商订单,统一转换为 JSON 格式
|
||||||
|
2. Gateway 通过 HTTP POST 回传给 CMP 系统
|
||||||
|
3. CMP 系统根据 `virtual_product_code` 匹配号卡
|
||||||
|
4. CMP 系统创建订单记录(`order_type` 为 "number_card")
|
||||||
|
5. 如果有 `agent_id`,触发代理分佣流程
|
||||||
|
|
||||||
|
#### Scenario: 接收运营商订单回传
|
||||||
|
|
||||||
|
- **WHEN** Gateway 回传运营商订单,虚拟商品编码为 "VC-CMCC-001",代理 ID 为 123,订单金额为 30.00 元
|
||||||
|
- **THEN** 系统创建订单记录,`order_type` 为 "number_card",`source_id` 为号卡 ID,`agent_id` 为 123,触发分佣计算
|
||||||
|
|
||||||
|
#### Scenario: 虚拟商品编码不存在
|
||||||
|
|
||||||
|
- **WHEN** Gateway 回传运营商订单,虚拟商品编码为不存在的 "VC-UNKNOWN"
|
||||||
|
- **THEN** 系统拒绝创建订单,返回错误信息"虚拟商品编码不存在"并记录到日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡代理分销
|
||||||
|
|
||||||
|
系统 SHALL 支持号卡的代理分销,代理通过推广链接或卡板推广号卡给终端用户。
|
||||||
|
|
||||||
|
**分销规则**:
|
||||||
|
- 号卡由运营商定价,平台无权修改价格
|
||||||
|
- 代理通过推广链接或卡板获取用户激活
|
||||||
|
- 用户激活充值后,资金直接支付给运营商,不经过平台
|
||||||
|
- 运营商周期性结算总佣金给平台
|
||||||
|
- 平台根据代理分佣规则分配佣金给代理
|
||||||
|
|
||||||
|
**代理推广方式**:
|
||||||
|
- **推广链接**: 代理生成带有 `agent_id` 的推广链接,用户点击链接激活
|
||||||
|
- **卡板**: 代理线下分发印有二维码的卡板,用户扫码激活
|
||||||
|
|
||||||
|
#### Scenario: 代理生成推广链接
|
||||||
|
|
||||||
|
- **WHEN** 代理商(用户 ID 为 123)为号卡(ID 为 5001)生成推广链接
|
||||||
|
- **THEN** 系统生成带有 `agent_id=123` 和 `product_id=5001` 的推广链接,如 `https://example.com/activate?agent=123&product=5001`
|
||||||
|
|
||||||
|
#### Scenario: 用户通过代理链接激活
|
||||||
|
|
||||||
|
- **WHEN** 用户通过代理推广链接激活号卡并充值 30.00 元
|
||||||
|
- **THEN** 运营商接收用户支付,Gateway 回传订单时包含 `agent_id=123`,系统触发代理分佣流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡分佣处理
|
||||||
|
|
||||||
|
系统 SHALL 根据号卡分佣规则计算代理佣金,支持冻结和解冻流程。
|
||||||
|
|
||||||
|
**分佣规则**:
|
||||||
|
- 号卡分佣配置在代理分佣规则表(`commission_rules`)中
|
||||||
|
- 分佣类型:一次性分佣、长期分佣、组合分佣(参考 iot-agent-commission 规范)
|
||||||
|
- 号卡订单的分佣需要满足条件:激活(实名) + 达到充值金额 + 在网状态 + 三无校验
|
||||||
|
- 分佣记录创建时状态为"冻结",满足条件后变为"解冻中",审批通过后变为"已发放"
|
||||||
|
|
||||||
|
#### Scenario: 号卡订单触发分佣
|
||||||
|
|
||||||
|
- **WHEN** 运营商回传订单,代理 ID 为 123,订单金额为 30.00 元,该代理配置了一次性分佣 5.00 元
|
||||||
|
- **THEN** 系统创建分佣记录,金额为 5.00 元,状态为"冻结",等待满足解冻条件
|
||||||
|
|
||||||
|
#### Scenario: 号卡分佣解冻
|
||||||
|
|
||||||
|
- **WHEN** 号卡订单满足解冻条件(激活 + 充值 + 在网 + 三无校验)
|
||||||
|
- **THEN** 系统将分佣记录状态从"冻结"变更为"解冻中",创建分佣解冻审批记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡运营商结算
|
||||||
|
|
||||||
|
系统 SHALL 记录运营商周期性结算的佣金总额,用于财务对账和利润计算。
|
||||||
|
|
||||||
|
**结算字段**:
|
||||||
|
- `settlement_id`: 结算记录 ID(主键,BIGINT)
|
||||||
|
- `carrier`: 运营商名称(VARCHAR(100))
|
||||||
|
- `settlement_period`: 结算周期(VARCHAR(50),如 "2025-01")
|
||||||
|
- `total_commission`: 运营商结算的佣金总额(DECIMAL(18,2))
|
||||||
|
- `settlement_time`: 结算时间(TIMESTAMP)
|
||||||
|
- `status`: 结算状态(INT,1-待确认 2-已确认)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 记录运营商结算
|
||||||
|
|
||||||
|
- **WHEN** 运营商"中国移动"结算 2025 年 1 月的佣金总额 50000.00 元
|
||||||
|
- **THEN** 系统创建结算记录,`carrier` 为 "中国移动",`settlement_period` 为 "2025-01",`total_commission` 为 50000.00,状态为 1(待确认)
|
||||||
|
|
||||||
|
#### Scenario: 确认运营商结算
|
||||||
|
|
||||||
|
- **WHEN** 财务确认运营商结算记录(ID 为 1001)
|
||||||
|
- **THEN** 系统将结算记录状态从 1(待确认) 变更为 2(已确认)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对号卡数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 虚拟商品编码(virtual_product_code):必填,长度 1-100 字符,唯一
|
||||||
|
- 商品名称(product_name):必填,长度 1-255 字符
|
||||||
|
- 运营商名称(carrier):必填,长度 1-100 字符
|
||||||
|
- 固定售价(price):必填,≥ 0,最多 2 位小数
|
||||||
|
- 状态(status):必填,枚举值 1(上架) | 2(下架)
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡时虚拟商品编码为空
|
||||||
|
|
||||||
|
- **WHEN** 平台创建号卡,虚拟商品编码为空
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"虚拟商品编码不能为空"
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡时固定售价为负数
|
||||||
|
|
||||||
|
- **WHEN** 平台创建号卡,固定售价为 -10.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"固定售价必须 ≥ 0"
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 订单实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义订单(Order)实体,统一管理两种订单类型:套餐订单、号卡订单。
|
||||||
|
|
||||||
|
**核心概念**:
|
||||||
|
- **套餐订单**: 用户为 IoT 卡或设备购买套餐的订单,包括单卡套餐订单和设备级套餐订单
|
||||||
|
- **号卡订单**: 运营商回传的号卡订单,用户直接在上游平台下单,系统只接收订单状态更新
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
- `id`: 订单 ID(主键,BIGINT)
|
||||||
|
- `order_no`: 订单编号(VARCHAR(50),唯一)
|
||||||
|
- `order_type`: 订单类型(INT,1-套餐订单 2-号卡订单)
|
||||||
|
- `iot_card_id`: IoT 卡 ID(BIGINT,可空,单卡套餐订单时有值)
|
||||||
|
- `device_id`: 设备 ID(BIGINT,可空,设备级套餐订单时有值)
|
||||||
|
- `number_card_id`: 号卡 ID(BIGINT,可空,号卡订单时有值)
|
||||||
|
- `package_id`: 套餐 ID(BIGINT,可空,仅当 order_type 为 1 时有值)
|
||||||
|
- `user_id`: 用户 ID(BIGINT,购买用户)
|
||||||
|
- `agent_id`: 代理 ID(BIGINT,可空,通过代理购买时有值)
|
||||||
|
- `amount`: 订单金额(DECIMAL(10,2),元)
|
||||||
|
- `payment_method`: 支付方式(VARCHAR(20),"wallet"-钱包 | "online"-在线支付 | "carrier"-运营商直付)
|
||||||
|
- `status`: 订单状态(INT,1-待支付 2-已支付 3-已完成 4-已取消 5-已退款)
|
||||||
|
- `carrier_order_id`: 运营商订单 ID(VARCHAR(255),可空,仅号卡订单有值)
|
||||||
|
- `carrier_order_data`: 运营商订单原始数据(JSONB,可空)
|
||||||
|
- `paid_at`: 支付时间(TIMESTAMP,可空)
|
||||||
|
- `completed_at`: 完成时间(TIMESTAMP,可空)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
**订单类型说明**:
|
||||||
|
- **单卡套餐订单**: `order_type` 为 1,`iot_card_id` 有值,`device_id` 为 NULL
|
||||||
|
- **设备级套餐订单**: `order_type` 为 1,`device_id` 有值,`iot_card_id` 为 NULL
|
||||||
|
- **号卡订单**: `order_type` 为 2,`number_card_id` 有值,`iot_card_id` 和 `device_id` 为 NULL
|
||||||
|
|
||||||
|
#### Scenario: 创建单卡套餐购买订单
|
||||||
|
|
||||||
|
- **WHEN** 用户(ID 为 2001)为 IoT 卡(ID 为 1001)购买套餐(ID 为 3001),金额为 30.00 元
|
||||||
|
- **THEN** 系统创建订单记录,`order_type` 为 1,`iot_card_id` 为 1001,`device_id` 为 NULL,`package_id` 为 3001,`user_id` 为 2001,`amount` 为 30.00,状态为 1(待支付)
|
||||||
|
|
||||||
|
#### Scenario: 创建设备级套餐购买订单
|
||||||
|
|
||||||
|
- **WHEN** 用户(ID 为 2001)为设备(ID 为 5001,绑定 3 张 IoT 卡)购买套餐(ID 为 3002),金额为 399.00 元
|
||||||
|
- **THEN** 系统创建订单记录,`order_type` 为 1,`device_id` 为 5001,`iot_card_id` 为 NULL,`package_id` 为 3002,`user_id` 为 2001,`amount` 为 399.00,状态为 1(待支付)
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡订单(运营商回传)
|
||||||
|
|
||||||
|
- **WHEN** Gateway 回传运营商订单,虚拟商品编码对应号卡 ID 为 6001,代理 ID 为 123,订单金额为 30.00 元
|
||||||
|
- **THEN** 系统创建订单记录,`order_type` 为 2,`number_card_id` 为 6001,`iot_card_id` 为 NULL,`device_id` 为 NULL,`agent_id` 为 123,`amount` 为 30.00,`payment_method` 为 "carrier",状态为 2(已支付)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单状态流转
|
||||||
|
|
||||||
|
系统 SHALL 管理订单的状态流转,确保状态变更符合业务规则。
|
||||||
|
|
||||||
|
**状态定义**:
|
||||||
|
- **1-待支付**: 订单已创建,等待用户支付
|
||||||
|
- **2-已支付**: 用户已支付,等待系统处理
|
||||||
|
- **3-已完成**: 订单已完成(激活/发货等)
|
||||||
|
- **4-已取消**: 订单已取消
|
||||||
|
- **5-已退款**: 订单已退款
|
||||||
|
|
||||||
|
**状态流转规则**:
|
||||||
|
- 待支付(1) → 已支付(2): 用户完成支付
|
||||||
|
- 待支付(1) → 已取消(4): 用户取消订单或订单超时
|
||||||
|
- 已支付(2) → 已完成(3): 系统完成订单处理(激活/发货)
|
||||||
|
- 已支付(2) → 已退款(5): 用户申请退款且审核通过
|
||||||
|
- 已完成(3) → 已退款(5): 用户申请退款且审核通过(特殊情况)
|
||||||
|
|
||||||
|
#### Scenario: 用户支付订单
|
||||||
|
|
||||||
|
- **WHEN** 用户支付待支付订单(ID 为 10001),支付金额为 30.00 元
|
||||||
|
- **THEN** 系统将订单状态从 1(待支付) 变更为 2(已支付),`paid_at` 记录支付时间
|
||||||
|
|
||||||
|
#### Scenario: 单卡套餐订单完成
|
||||||
|
|
||||||
|
- **WHEN** 系统处理完单卡套餐订单(ID 为 10001),激活 IoT 卡并分配套餐
|
||||||
|
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
|
||||||
|
|
||||||
|
#### Scenario: 设备级套餐订单完成
|
||||||
|
|
||||||
|
- **WHEN** 系统处理完设备级套餐订单(ID 为 10002),为设备绑定的所有 IoT 卡分配套餐
|
||||||
|
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单支付方式
|
||||||
|
|
||||||
|
系统 SHALL 支持三种支付方式:钱包支付、在线支付、运营商直付。
|
||||||
|
|
||||||
|
**支付方式**:
|
||||||
|
- **钱包支付(wallet)**: 从用户钱包余额扣款
|
||||||
|
- **在线支付(online)**: 通过第三方支付(微信/支付宝等)
|
||||||
|
- **运营商直付(carrier)**: 用户直接支付给运营商(仅号卡订单)
|
||||||
|
|
||||||
|
**支付规则**:
|
||||||
|
- 一次性分佣订单必须使用钱包支付
|
||||||
|
- 套餐购买订单可以使用钱包或在线支付
|
||||||
|
- 号卡订单必须使用运营商直付
|
||||||
|
|
||||||
|
#### Scenario: 钱包支付订单
|
||||||
|
|
||||||
|
- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 50.00 元
|
||||||
|
- **THEN** 系统从钱包扣除 30.00 元,订单状态变更为 2(已支付),`payment_method` 为 "wallet"
|
||||||
|
|
||||||
|
#### Scenario: 钱包余额不足
|
||||||
|
|
||||||
|
- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 20.00 元
|
||||||
|
- **THEN** 系统拒绝支付,返回错误信息"钱包余额不足"
|
||||||
|
|
||||||
|
#### Scenario: 一次性分佣订单强制钱包支付
|
||||||
|
|
||||||
|
- **WHEN** 用户购买配置了一次性分佣的套餐,尝试使用在线支付
|
||||||
|
- **THEN** 系统拒绝支付,返回错误信息"一次性分佣订单必须使用钱包支付"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单分佣触发
|
||||||
|
|
||||||
|
系统 SHALL 在订单完成时触发分佣计算,根据代理分佣规则创建分佣记录。
|
||||||
|
|
||||||
|
**触发条件**:
|
||||||
|
- 订单状态变更为 3(已完成)
|
||||||
|
- 订单有 `agent_id`(通过代理销售)
|
||||||
|
- 代理配置了分佣规则
|
||||||
|
|
||||||
|
**分佣计算规则**:
|
||||||
|
- **单卡套餐订单**: 根据 IoT 卡关联的代理分佣规则计算分佣
|
||||||
|
- **设备级套餐订单**: 分佣只计算一次(不按设备绑定的 IoT 卡数量倍增)
|
||||||
|
- **号卡订单**: 下单即冻结分佣,次月通过 Excel 导入解冻
|
||||||
|
|
||||||
|
#### Scenario: 单卡套餐购买订单触发分佣
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)的单卡套餐订单(ID 为 10001)完成,订单金额为 30.00 元,代理配置了 5.00 元一次性分佣
|
||||||
|
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结)
|
||||||
|
|
||||||
|
#### Scenario: 设备级套餐订单触发分佣(只计算一次)
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)的设备级套餐订单(ID 为 10002)完成,设备绑定 3 张 IoT 卡,订单金额为 399.00 元,代理配置了 100.00 元长期分佣
|
||||||
|
- **THEN** 系统创建一条分佣记录,`agent_id` 为 123,`order_id` 为 10002,`amount` 为 100.00,状态为 1(冻结),不是 3 × 100.00
|
||||||
|
|
||||||
|
#### Scenario: 号卡订单触发分佣
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)的号卡订单(ID 为 10003)创建,订单金额为 30.00 元,代理配置了长期分佣
|
||||||
|
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10003,状态为 1(冻结),等待次月通过 Excel 导入解冻
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单查询和筛选
|
||||||
|
|
||||||
|
系统 SHALL 支持多维度查询和筛选订单。
|
||||||
|
|
||||||
|
**查询条件**:
|
||||||
|
- 订单编号(精确匹配)
|
||||||
|
- 订单类型(1-套餐订单 2-号卡订单)
|
||||||
|
- 订单状态(单选或多选)
|
||||||
|
- IoT 卡 ID(精确匹配)
|
||||||
|
- 设备 ID(精确匹配)
|
||||||
|
- 号卡 ID(精确匹配)
|
||||||
|
- 用户 ID(精确匹配)
|
||||||
|
- 代理 ID(精确匹配)
|
||||||
|
- 支付方式(单选或多选)
|
||||||
|
- 创建时间范围(开始时间 - 结束时间)
|
||||||
|
- 支付时间范围(开始时间 - 结束时间)
|
||||||
|
- 完成时间范围(开始时间 - 结束时间)
|
||||||
|
|
||||||
|
**分页**:
|
||||||
|
- 默认每页 20 条,最大每页 100 条
|
||||||
|
- 返回总记录数和总页数
|
||||||
|
|
||||||
|
#### Scenario: 查询用户的所有订单
|
||||||
|
|
||||||
|
- **WHEN** 用户(ID 为 2001)查询自己的所有订单
|
||||||
|
- **THEN** 系统返回 `user_id` 为 2001 的所有订单列表,按创建时间倒序排列
|
||||||
|
|
||||||
|
#### Scenario: 查询代理的订单
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)查询自己的订单,筛选已完成的套餐订单
|
||||||
|
- **THEN** 系统返回 `agent_id` 为 123 且 `order_type` 为 1 且 `status` 为 3(已完成) 的订单列表
|
||||||
|
|
||||||
|
#### Scenario: 查询 IoT 卡的订单历史
|
||||||
|
|
||||||
|
- **WHEN** 运营人员查询 IoT 卡(ID 为 1001)的所有订单
|
||||||
|
- **THEN** 系统返回 `iot_card_id` 为 1001 的所有订单列表,包含套餐购买记录
|
||||||
|
|
||||||
|
#### Scenario: 查询设备的订单历史
|
||||||
|
|
||||||
|
- **WHEN** 运营人员查询设备(ID 为 5001)的所有订单
|
||||||
|
- **THEN** 系统返回 `device_id` 为 5001 的所有设备级套餐订单列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对订单数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 订单编号(order_no):必填,长度 1-50 字符,唯一
|
||||||
|
- 订单类型(order_type):必填,枚举值 1(套餐订单) | 2(号卡订单)
|
||||||
|
- IoT 卡 ID(iot_card_id):套餐订单时 iot_card_id 和 device_id 二选一
|
||||||
|
- 设备 ID(device_id):套餐订单时 iot_card_id 和 device_id 二选一
|
||||||
|
- 号卡 ID(number_card_id):号卡订单时必填
|
||||||
|
- 套餐 ID(package_id):套餐订单时必填
|
||||||
|
- 用户 ID(user_id):必填,≥ 1
|
||||||
|
- 订单金额(amount):必填,≥ 0,最多 2 位小数
|
||||||
|
- 支付方式(payment_method):必填,枚举值 "wallet" | "online" | "carrier"
|
||||||
|
- 状态(status):必填,枚举值 1-5
|
||||||
|
|
||||||
|
#### Scenario: 创建订单时金额为负数
|
||||||
|
|
||||||
|
- **WHEN** 创建订单,金额为 -10.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"订单金额必须 ≥ 0"
|
||||||
|
|
||||||
|
#### Scenario: 创建订单时订单编号重复
|
||||||
|
|
||||||
|
- **WHEN** 创建订单,订单编号为已存在的 "ORD-2025-001"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"订单编号已存在"
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐订单时未关联 IoT 卡或设备
|
||||||
|
|
||||||
|
- **WHEN** 创建套餐订单,`iot_card_id` 和 `device_id` 都为 NULL
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"套餐订单必须关联 IoT 卡或设备"
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐订单时同时关联 IoT 卡和设备
|
||||||
|
|
||||||
|
- **WHEN** 创建套餐订单,`iot_card_id` 为 1001,`device_id` 为 5001
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"套餐订单不能同时关联 IoT 卡和设备"
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡订单时未关联号卡
|
||||||
|
|
||||||
|
- **WHEN** 创建号卡订单,`number_card_id` 为 NULL
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"号卡订单必须关联号卡"
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 套餐实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义套餐(Package)实体,包含套餐的基本属性、定价、流量配置。
|
||||||
|
|
||||||
|
**核心概念**: 套餐只适用于 IoT 卡(ICCID),用户可以为单张 IoT 卡购买套餐,也可以为设备购买套餐(套餐分配到设备绑定的所有 IoT 卡,流量设备级共享)。
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
- `id`: 套餐 ID(主键,BIGINT)
|
||||||
|
- `package_code`: 套餐编码(VARCHAR(50),唯一)
|
||||||
|
- `package_name`: 套餐名称(VARCHAR(255))
|
||||||
|
- `series_id`: 套餐系列 ID(BIGINT,关联 package_series 表,用于组织套餐分组和配置一次性分佣)
|
||||||
|
- `package_type`: 套餐类型(VARCHAR(20),"formal"-正式套餐 | "addon"-加油包)
|
||||||
|
- `duration_months`: 套餐时长(INT,月数,1-月套餐 12-年套餐,加油包为 0)
|
||||||
|
- `real_data_mb`: 真流量额度(BIGINT,MB 为单位,可选)
|
||||||
|
- `virtual_data_mb`: 虚流量额度(BIGINT,MB 为单位,用于停机判断,可选)
|
||||||
|
- `data_amount_mb`: 总流量额度(BIGINT,MB 为单位,real_data_mb + virtual_data_mb)
|
||||||
|
- `price`: 套餐价格(DECIMAL(10,2),元)
|
||||||
|
- `status`: 套餐状态(INT,1-上架 2-下架)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
**套餐类型说明**:
|
||||||
|
- **正式套餐(formal)**: 每张 IoT 卡只能有一个有效的正式套餐,购买新的正式套餐会替换旧的
|
||||||
|
- **加油包(addon)**: 每张 IoT 卡可以购买多个加油包,与正式套餐共存
|
||||||
|
|
||||||
|
#### Scenario: 创建月套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建月套餐,套餐编码为 "PKG-M-001",套餐名称为 "月套餐 10GB",套餐系列 ID 为 1,类型为正式套餐,时长为 1 个月,真流量为 10240 MB,虚流量为 0,价格为 30.00 元
|
||||||
|
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-M-001",`series_id` 为 1,`package_type` 为 "formal",`duration_months` 为 1,`real_data_mb` 为 10240,`virtual_data_mb` 为 0,`data_amount_mb` 为 10240,`price` 为 30.00
|
||||||
|
|
||||||
|
#### Scenario: 创建年套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建年套餐,套餐编码为 "PKG-Y-001",套餐名称为 "年套餐 120GB",套餐系列 ID 为 1,类型为正式套餐,时长为 12 个月,真流量为 122880 MB,虚流量为 0,价格为 300.00 元
|
||||||
|
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-Y-001",`series_id` 为 1,`package_type` 为 "formal",`duration_months` 为 12,`real_data_mb` 为 122880,`virtual_data_mb` 为 0,`data_amount_mb` 为 122880,`price` 为 300.00
|
||||||
|
|
||||||
|
#### Scenario: 创建流量加油包
|
||||||
|
|
||||||
|
- **WHEN** 平台创建加油包,套餐编码为 "PKG-ADD-001",套餐名称为 "流量包 5GB",套餐系列 ID 为 2,类型为加油包,时长为 0,真流量为 5120 MB,虚流量为 0,价格为 10.00 元
|
||||||
|
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-ADD-001",`series_id` 为 2,`package_type` 为 "addon",`duration_months` 为 0,`real_data_mb` 为 5120,`virtual_data_mb` 为 0,`data_amount_mb` 为 5120,`price` 为 10.00
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 套餐流量类型和真虚流量共存
|
||||||
|
|
||||||
|
系统 SHALL 支持真流量和虚流量两种流量类型,两者可以共存于同一套餐中。
|
||||||
|
|
||||||
|
**流量类型定义**:
|
||||||
|
- **真流量(real_data_mb)**: 实际可用的流量,可在运营商网络中使用
|
||||||
|
- **虚流量(virtual_data_mb)**: 虚拟流量,用于停机判断(虚流量用完后停机,即使真流量还有剩余)
|
||||||
|
- **总流量(data_amount_mb)**: 真流量 + 虚流量的总和
|
||||||
|
|
||||||
|
**重要规则**:
|
||||||
|
- 真流量和虚流量可以同时存在于一个套餐中
|
||||||
|
- 停机判断基于虚流量(虚流量用完后停机)
|
||||||
|
- 套餐可以只有真流量、只有虚流量、或两者都有
|
||||||
|
|
||||||
|
#### Scenario: 创建真虚流量共存的套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,真流量为 8000 MB,虚流量为 2000 MB
|
||||||
|
- **THEN** 系统创建套餐记录,`real_data_mb` 为 8000,`virtual_data_mb` 为 2000,`data_amount_mb` 为 10000
|
||||||
|
|
||||||
|
#### Scenario: 创建纯真流量套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,真流量为 10240 MB,虚流量为 0
|
||||||
|
- **THEN** 系统创建套餐记录,`real_data_mb` 为 10240,`virtual_data_mb` 为 0,`data_amount_mb` 为 10240
|
||||||
|
|
||||||
|
#### Scenario: 创建纯虚流量套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,真流量为 0,虚流量为 10240 MB
|
||||||
|
- **THEN** 系统创建套餐记录,`real_data_mb` 为 0,`virtual_data_mb` 为 10240,`data_amount_mb` 为 10240
|
||||||
|
|
||||||
|
#### Scenario: 虚流量用完停机
|
||||||
|
|
||||||
|
- **WHEN** 套餐的虚流量为 2000 MB,用户已使用 2000 MB 虚流量,但真流量还剩余 5000 MB
|
||||||
|
- **THEN** 系统判断虚流量已用完,触发停机操作,即使真流量还有剩余
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 单卡套餐购买
|
||||||
|
|
||||||
|
系统 SHALL 支持用户为单张 IoT 卡购买套餐。
|
||||||
|
|
||||||
|
**购买规则**:
|
||||||
|
- 每张 IoT 卡只能有一个有效的正式套餐
|
||||||
|
- 购买新的正式套餐会替换旧的正式套餐
|
||||||
|
- 可以同时购买多个加油包
|
||||||
|
- 套餐购买后创建套餐订单记录
|
||||||
|
|
||||||
|
#### Scenario: 为 IoT 卡购买正式套餐
|
||||||
|
|
||||||
|
- **WHEN** 用户为 IoT 卡(ICCID 为 "8986...")购买月套餐(套餐 ID 为 1001),价格为 30.00 元
|
||||||
|
- **THEN** 系统创建套餐订单,`order_type` 为 1(套餐订单),`iot_card_id` 为 IoT 卡 ID,`package_id` 为 1001,`amount` 为 30.00
|
||||||
|
|
||||||
|
#### Scenario: 为 IoT 卡购买加油包
|
||||||
|
|
||||||
|
- **WHEN** 用户为 IoT 卡购买流量加油包(套餐 ID 为 2001),价格为 10.00 元
|
||||||
|
- **THEN** 系统创建套餐订单,IoT 卡的正式套餐保持不变,加油包作为额外套餐生效
|
||||||
|
|
||||||
|
#### Scenario: 购买新正式套餐替换旧套餐
|
||||||
|
|
||||||
|
- **WHEN** 用户为 IoT 卡购买新的月套餐,该 IoT 卡已有月套餐
|
||||||
|
- **THEN** 系统创建新订单,旧的正式套餐失效,新套餐生效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备级套餐购买和流量共享
|
||||||
|
|
||||||
|
系统 SHALL 支持用户为设备购买套餐,套餐分配到设备绑定的所有 IoT 卡,流量设备级共享。
|
||||||
|
|
||||||
|
**设备套餐业务规则**:
|
||||||
|
- 用户为设备购买套餐时,套餐会分配到设备绑定的**所有 IoT 卡**(1-4 张)
|
||||||
|
- 套餐的流量是**设备级别共享的**(例如 3000G/月共享,不管用哪张卡)
|
||||||
|
- 分佣**只计算一次**(不按卡数倍增)
|
||||||
|
- 订单表通过 `device_id` 字段关联设备,通过 `device_sim_bindings` 表查找绑定的所有 IoT 卡
|
||||||
|
- 设备购买的套餐不受单卡套餐限制(设备套餐和单卡套餐独立管理)
|
||||||
|
|
||||||
|
**流量共享机制**:
|
||||||
|
- 设备绑定的所有 IoT 卡共享套餐流量池
|
||||||
|
- 任意一张 IoT 卡使用流量都会从共享池扣除
|
||||||
|
- 流量池耗尽后,所有绑定的 IoT 卡都无法使用
|
||||||
|
|
||||||
|
**订单记录**:
|
||||||
|
- 订单表 `device_id` 字段记录设备 ID(设备级套餐订单)
|
||||||
|
- 订单表 `iot_card_id` 字段为 NULL(不关联具体 IoT 卡)
|
||||||
|
- 通过 `device_sim_bindings` 表查询设备绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
#### Scenario: 为设备购买套餐
|
||||||
|
|
||||||
|
- **WHEN** 用户为设备(ID 为 1001,绑定 3 张 IoT 卡)购买年套餐,价格为 399.00 元,流量为 3000G/月
|
||||||
|
- **THEN** 系统创建套餐订单,`order_type` 为 1(套餐订单),`device_id` 为 1001,`iot_card_id` 为 NULL,`amount` 为 399.00,套餐分配到 3 张绑定的 IoT 卡
|
||||||
|
|
||||||
|
#### Scenario: 设备流量共享
|
||||||
|
|
||||||
|
- **WHEN** 设备(绑定 3 张 IoT 卡)购买套餐 3000G/月,其中一张 IoT 卡使用 1000G 流量
|
||||||
|
- **THEN** 流量池剩余 2000G,其他两张 IoT 卡可以使用剩余的 2000G
|
||||||
|
|
||||||
|
#### Scenario: 设备套餐分佣只计算一次
|
||||||
|
|
||||||
|
- **WHEN** 设备(绑定 3 张 IoT 卡)购买套餐,长期佣金为 100.00 元
|
||||||
|
- **THEN** 系统创建一条分佣记录,金额为 100.00 元(不是 3 × 100.00 元)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 套餐分配给代理
|
||||||
|
|
||||||
|
系统 SHALL 支持将套餐分配给代理商,代理可以在平台设置的成本价基础上加价销售。
|
||||||
|
|
||||||
|
**分配规则**:
|
||||||
|
- 平台为套餐设置成本价(分配给代理的价格)
|
||||||
|
- 代理可以在成本价基础上加价,但不能超过成本价的 2 倍
|
||||||
|
- 分配记录存储在 `agent_package_allocations` 表
|
||||||
|
|
||||||
|
**agent_package_allocations 表**:
|
||||||
|
- `id`: 分配记录 ID(主键,BIGINT)
|
||||||
|
- `agent_id`: 代理用户 ID(BIGINT)
|
||||||
|
- `package_id`: 套餐 ID(BIGINT)
|
||||||
|
- `cost_price`: 成本价(DECIMAL(10,2),平台给代理的价格)
|
||||||
|
- `retail_price`: 零售价(DECIMAL(10,2),代理设置的终端销售价格)
|
||||||
|
- `status`: 分配状态(INT,1-有效 2-无效)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 平台分配套餐给代理
|
||||||
|
|
||||||
|
- **WHEN** 平台将套餐(ID 为 1001)分配给代理(用户 ID 为 123),成本价为 25.00 元
|
||||||
|
- **THEN** 系统创建分配记录,`agent_id` 为 123,`package_id` 为 1001,`cost_price` 为 25.00,状态为 1(有效)
|
||||||
|
|
||||||
|
#### Scenario: 代理设置零售价
|
||||||
|
|
||||||
|
- **WHEN** 代理(用户 ID 为 123)为套餐(ID 为 1001)设置零售价为 30.00 元
|
||||||
|
- **THEN** 系统更新分配记录,`retail_price` 为 30.00
|
||||||
|
|
||||||
|
#### Scenario: 代理零售价超过 2 倍成本价
|
||||||
|
|
||||||
|
- **WHEN** 代理设置零售价为 60.00 元,成本价为 25.00 元(2 倍为 50.00 元)
|
||||||
|
- **THEN** 系统拒绝设置,返回错误信息"零售价不能超过成本价的 2 倍"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 套餐数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对套餐数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 套餐编码(package_code):必填,长度 1-50 字符,唯一
|
||||||
|
- 套餐名称(package_name):必填,长度 1-255 字符
|
||||||
|
- 套餐系列 ID(series_id):必填,≥ 1,必须是有效的套餐系列 ID
|
||||||
|
- 套餐类型(package_type):必填,枚举值 "formal" | "addon"
|
||||||
|
- 套餐时长(duration_months):必填,≥ 0(正式套餐 ≥ 1,加油包为 0)
|
||||||
|
- 真流量额度(real_data_mb):可选,≥ 0
|
||||||
|
- 虚流量额度(virtual_data_mb):可选,≥ 0
|
||||||
|
- 总流量额度(data_amount_mb):必填,≥ 0,必须等于 real_data_mb + virtual_data_mb
|
||||||
|
- 套餐价格(price):必填,≥ 0,最多 2 位小数
|
||||||
|
- 状态(status):必填,枚举值 1(上架) | 2(下架)
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐时价格为负数
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,价格为 -10.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"套餐价格必须 ≥ 0"
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐时套餐编码重复
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,套餐编码为已存在的 "PKG-M-001"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"套餐编码已存在"
|
||||||
|
|
||||||
|
#### Scenario: 创建正式套餐时时长为 0
|
||||||
|
|
||||||
|
- **WHEN** 平台创建正式套餐,套餐类型为 "formal",时长为 0
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"正式套餐时长必须 ≥ 1"
|
||||||
441
openspec/changes/archive/2026-01-12-iot-sim-management/tasks.md
Normal file
441
openspec/changes/archive/2026-01-12-iot-sim-management/tasks.md
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
# IoT SIM 管理 - 数据模型与数据库表结构实现任务
|
||||||
|
|
||||||
|
本任务清单聚焦于 IoT SIM 管理模块的数据模型定义和数据库表结构实现,不包含业务逻辑代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 数据库迁移脚本
|
||||||
|
|
||||||
|
### 1.1 核心业务表
|
||||||
|
- [x] 1.1.1 创建迁移脚本文件:`migrations/YYYYMMDDHHMMSS_create_iot_sim_management_tables.up.sql` 和 `*.down.sql`
|
||||||
|
- [x] 1.1.2 创建运营商表(carriers)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `carrier_code` 唯一索引
|
||||||
|
- 初始数据:中国移动(CMCC)、中国联通(CUCC)、中国电信(CTCC)
|
||||||
|
- [x] 1.1.3 创建 IoT 卡表(iot_cards)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `iccid` 唯一索引
|
||||||
|
- `card_category` 字段:枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
|
||||||
|
- `carrier_id` 索引(关联运营商表)
|
||||||
|
- `owner_type` + `owner_id` + `status` 组合索引
|
||||||
|
- `batch_no` 索引
|
||||||
|
- `activated_at` 索引
|
||||||
|
- `card_category` 索引(用于区分普通卡和行业卡)
|
||||||
|
- `enable_polling` + `activation_status` + `last_data_check_at` 组合索引(卡流量轮询查询优化)
|
||||||
|
- `enable_polling` + `real_name_status` + `last_real_name_check_at` 组合索引(实名轮询查询优化)
|
||||||
|
- [x] 1.1.4 创建设备表(devices)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `device_no` 唯一索引
|
||||||
|
- `owner_type` + `owner_id` + `status` 组合索引
|
||||||
|
- [x] 1.1.5 创建号卡表(number_cards)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `virtual_product_code` 唯一索引
|
||||||
|
- `agent_id` + `status` 组合索引
|
||||||
|
- [x] 1.1.6 创建套餐系列表(package_series)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `series_code` 唯一索引
|
||||||
|
- [x] 1.1.7 创建套餐表(packages)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `package_code` 唯一索引
|
||||||
|
- `series_id` + `status` 组合索引
|
||||||
|
- [x] 1.1.8 创建代理套餐分配表(agent_package_allocations)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `agent_id` + `package_id` 唯一组合索引
|
||||||
|
- [x] 1.1.9 创建设备-IoT卡绑定关系表(device_sim_bindings)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `device_id` + `bind_status` 组合索引
|
||||||
|
- `iot_card_id` + `bind_status` 组合索引
|
||||||
|
- `iot_card_id` 部分唯一索引(WHERE bind_status = 1)
|
||||||
|
- [x] 1.1.10 创建订单表(orders)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `order_no` 唯一索引
|
||||||
|
- `user_id` + `status` 组合索引
|
||||||
|
- `agent_id` + `status` 组合索引
|
||||||
|
- `iot_card_id` 索引
|
||||||
|
- `device_id` 索引
|
||||||
|
- `number_card_id` 索引
|
||||||
|
|
||||||
|
### 1.2 套餐和轮询相关表
|
||||||
|
- [x] 1.2.1 创建套餐使用情况表(package_usages)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `order_id` 索引
|
||||||
|
- `package_id` 索引
|
||||||
|
- `iot_card_id` 索引
|
||||||
|
- `device_id` 索引
|
||||||
|
- `status` + `expires_at` + `last_package_check_at` 组合索引(套餐流量检查优化)
|
||||||
|
- [x] 1.2.2 创建轮询配置表(polling_configs)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `config_name` 唯一索引
|
||||||
|
- `status` + `card_condition` + `carrier_id` + `priority` 组合索引(配置匹配优化)
|
||||||
|
- [x] 1.2.3 创建流量使用记录表(data_usage_records)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `iot_card_id` + `check_time` 组合索引(按卡和时间查询)
|
||||||
|
- `check_time` 索引(按时间范围查询)
|
||||||
|
- 注意:此表数据量会快速增长,建议定期清理 90 天前的记录或使用分区表
|
||||||
|
|
||||||
|
### 1.3 分佣相关表
|
||||||
|
- [x] 1.3.1 创建代理层级关系表(agent_hierarchies)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `agent_id` 唯一索引
|
||||||
|
- `parent_agent_id` 索引
|
||||||
|
- [x] 1.3.2 创建分佣规则表(commission_rules)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `agent_id` + `business_type` + `card_type` 组合索引
|
||||||
|
- [x] 1.3.3 创建阶梯分佣配置表(commission_ladder)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `rule_id` 索引
|
||||||
|
- [x] 1.3.4 创建组合分佣条件表(commission_combined_conditions)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `rule_id` 唯一索引
|
||||||
|
- [x] 1.3.5 创建分佣记录表(commission_records)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `agent_id` + `status` 组合索引
|
||||||
|
- `order_id` 索引
|
||||||
|
- `rule_id` 索引
|
||||||
|
- [x] 1.3.6 创建分佣审批表(commission_approvals)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `commission_record_id` 索引
|
||||||
|
- `status` 索引
|
||||||
|
- [x] 1.3.7 创建分佣模板表(commission_templates)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `template_name` 唯一索引
|
||||||
|
- [x] 1.3.8 创建号卡运营商结算表(carrier_settlements)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `commission_record_id` 唯一索引
|
||||||
|
- `agent_id` + `status` 组合索引
|
||||||
|
|
||||||
|
### 1.4 财务管理表
|
||||||
|
- [x] 1.4.1 创建佣金提现申请表(commission_withdrawal_requests)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `agent_id` + `status` 组合索引
|
||||||
|
- `created_at` 索引
|
||||||
|
- [x] 1.4.2 创建佣金提现设置表(commission_withdrawal_settings)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `status` 索引
|
||||||
|
- [x] 1.4.3 创建收款商户设置表(payment_merchant_settings)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `user_id` + `is_default` 组合索引
|
||||||
|
- `merchant_type` + `status` 组合索引
|
||||||
|
|
||||||
|
### 1.5 系统管理表
|
||||||
|
- [x] 1.5.1 创建开发能力配置表(dev_capability_configs)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `app_id` 唯一索引
|
||||||
|
- `user_id` + `status` 组合索引
|
||||||
|
- [x] 1.5.2 创建换卡申请表(card_replacement_requests)及其索引
|
||||||
|
- 主键索引
|
||||||
|
- `user_id` + `status` 组合索引
|
||||||
|
- `old_iccid` 索引
|
||||||
|
- `new_iccid` 索引
|
||||||
|
|
||||||
|
### 1.6 迁移脚本验证
|
||||||
|
- [x] 1.6.1 编写迁移脚本的 down 部分(删除所有表)
|
||||||
|
- [x] 1.6.2 在本地测试数据库执行 up 迁移
|
||||||
|
- [x] 1.6.3 验证所有表和索引创建成功
|
||||||
|
- [x] 1.6.4 执行 down 迁移验证回滚成功
|
||||||
|
- [x] 1.6.5 编写迁移脚本的 README 说明(执行步骤、注意事项)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. GORM 模型定义
|
||||||
|
|
||||||
|
### 2.1 目录结构
|
||||||
|
- [x] 2.1.1 创建 `internal/iot/model` 目录
|
||||||
|
- [x] 2.1.2 创建模型文件结构:
|
||||||
|
- `carrier.go` - 运营商模型
|
||||||
|
- `iot_card.go` - IoT 卡模型
|
||||||
|
- `device.go` - 设备模型
|
||||||
|
- `number_card.go` - 号卡模型
|
||||||
|
- `package.go` - 套餐、套餐系列、套餐使用情况模型
|
||||||
|
- `order.go` - 订单模型
|
||||||
|
- `polling.go` - 轮询配置模型
|
||||||
|
- `data_usage.go` - 流量使用记录模型
|
||||||
|
- `commission.go` - 分佣相关模型
|
||||||
|
- `financial.go` - 财务管理模型
|
||||||
|
- `system.go` - 系统管理模型
|
||||||
|
|
||||||
|
### 2.2 核心业务模型
|
||||||
|
- [x] 2.2.1 定义运营商(Carrier)模型
|
||||||
|
- 字段包括:id, carrier_code, carrier_name, description, status, created_at, updated_at
|
||||||
|
- 初始数据:中国移动(CMCC)、中国联通(CUCC)、中国电信(CTCC)
|
||||||
|
- [x] 2.2.2 定义 IoT 卡(IotCard)模型
|
||||||
|
- 所有字段必须显式指定 `gorm:"column:字段名"`
|
||||||
|
- 添加中文字段注释(comment 标签)
|
||||||
|
- 字段包括:id, iccid, card_type, card_category, carrier_id, imsi, msisdn, batch_no, supplier, cost_price, distribute_price, status, owner_type, owner_id, activated_at, activation_status, real_name_status, network_status, data_usage_mb, enable_polling, last_data_check_at, last_real_name_check_at, last_sync_time, created_at, updated_at
|
||||||
|
- **关键调整**:
|
||||||
|
- `card_category` 字段:枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
|
||||||
|
- `carrier_id` 关联运营商表(替代原来的 carrier 字符串字段)
|
||||||
|
- `enable_polling` 控制是否参与轮询(默认 true)
|
||||||
|
- `last_data_check_at` 卡流量检查时间
|
||||||
|
- `last_real_name_check_at` 实名检查时间
|
||||||
|
- 行业卡可以在 `real_name_status` 为 0 的情况下激活使用
|
||||||
|
- [x] 2.2.3 定义设备(Device)模型
|
||||||
|
- 字段包括:id, device_no, device_name, device_model, device_type, max_sim_slots, manufacturer, batch_no, owner_type, owner_id, status, activated_at, device_username, device_password_encrypted, device_api_endpoint, created_at, updated_at
|
||||||
|
- [x] 2.2.4 定义号卡(NumberCard)模型
|
||||||
|
- 字段包括:id, virtual_product_code, card_name, card_type, carrier, data_amount_mb, price, agent_id, status, created_at, updated_at
|
||||||
|
- [x] 2.2.5 定义套餐系列(PackageSeries)模型
|
||||||
|
- 字段包括:id, series_code, series_name, description, status, created_at, updated_at
|
||||||
|
- [x] 2.2.6 定义套餐(Package)模型
|
||||||
|
- 字段包括:id, package_code, package_name, series_id, package_type, duration_months, data_type, real_data_mb, virtual_data_mb, data_amount_mb, price, status, created_at, updated_at
|
||||||
|
- [x] 2.2.7 定义代理套餐分配(AgentPackageAllocation)模型
|
||||||
|
- 字段包括:id, agent_id, package_id, cost_price, retail_price, status, created_at, updated_at
|
||||||
|
- [x] 2.2.8 定义设备-IoT卡绑定关系(DeviceSimBinding)模型
|
||||||
|
- 字段包括:id, device_id, iot_card_id, slot_number, bind_status, bound_at, unbound_at, created_at, updated_at
|
||||||
|
- [x] 2.2.9 定义订单(Order)模型
|
||||||
|
- 字段包括:id, order_no, order_type, iot_card_id, device_id, number_card_id, package_id, user_id, agent_id, amount, payment_method, status, carrier_order_id, carrier_order_data, paid_at, completed_at, created_at, updated_at
|
||||||
|
|
||||||
|
### 2.3 套餐和轮询相关模型
|
||||||
|
- [x] 2.3.1 定义套餐使用情况(PackageUsage)模型
|
||||||
|
- 字段包括:id, order_id, package_id, usage_type, iot_card_id, device_id, data_limit_mb, data_usage_mb, real_data_usage_mb, virtual_data_usage_mb, activated_at, expires_at, status, last_package_check_at, created_at, updated_at
|
||||||
|
- **业务逻辑**:
|
||||||
|
- `usage_type` = "single_card" 时,`iot_card_id` 有值,`device_id` 为 NULL
|
||||||
|
- `usage_type` = "device" 时,`device_id` 有值,`iot_card_id` 为 NULL
|
||||||
|
- `data_usage_mb` 通过汇总卡的流量计算(单卡套餐直接读卡流量,设备级套餐汇总所有卡流量)
|
||||||
|
- [x] 2.3.2 定义轮询配置(PollingConfig)模型
|
||||||
|
- 字段包括:id, config_name, description, card_condition, carrier_id, real_name_check_enabled, real_name_check_interval, card_data_check_enabled, card_data_check_interval, package_check_enabled, package_check_interval, priority, status, created_at, updated_at
|
||||||
|
- **配置说明**:
|
||||||
|
- `carrier_id` 为 NULL 表示匹配所有运营商
|
||||||
|
- `priority` 数字越小优先级越高
|
||||||
|
- 支持独立配置实名检查、卡流量检查、套餐流量检查
|
||||||
|
- [x] 2.3.3 定义流量使用记录(DataUsageRecord)模型
|
||||||
|
- 字段包括:id, iot_card_id, data_usage_mb, data_increase_mb, check_time, source, created_at
|
||||||
|
- **业务逻辑**:
|
||||||
|
- Worker 每次轮询卡流量后插入一条记录
|
||||||
|
- `data_increase_mb` = 本次流量 - 上次流量
|
||||||
|
- `source` 数据来源(polling-轮询 manual-手动 gateway-回调)
|
||||||
|
|
||||||
|
### 2.4 分佣相关模型
|
||||||
|
- [x] 2.4.1 定义代理层级关系(AgentHierarchy)模型
|
||||||
|
- 字段包括:id, agent_id, parent_agent_id, agent_path, level, created_at, updated_at
|
||||||
|
- [x] 2.4.2 定义分佣规则(CommissionRule)模型
|
||||||
|
- 字段包括:id, agent_id, business_type, card_type, commission_type, commission_mode, commission_value, unfreeze_days, min_activation_for_unfreeze, approval_type, status, created_at, updated_at
|
||||||
|
- [x] 2.4.3 定义阶梯分佣配置(CommissionLadder)模型
|
||||||
|
- 字段包括:id, rule_id, ladder_type, threshold_value, commission_mode, commission_value, created_at, updated_at
|
||||||
|
- [x] 2.4.4 定义组合分佣条件(CommissionCombinedCondition)模型
|
||||||
|
- 字段包括:id, rule_id, one_time_commission_mode, one_time_commission_value, long_term_commission_mode, long_term_commission_value, long_term_unfreeze_days, long_term_min_activation, created_at, updated_at
|
||||||
|
- [x] 2.4.5 定义分佣记录(CommissionRecord)模型
|
||||||
|
- 字段包括:id, agent_id, order_id, rule_id, commission_type, amount, status, unfrozen_at, released_at, created_at, updated_at
|
||||||
|
- [x] 2.4.6 定义分佣审批(CommissionApproval)模型
|
||||||
|
- 字段包括:id, commission_record_id, approver_id, status, reason, created_at, updated_at
|
||||||
|
- [x] 2.4.7 定义分佣模板(CommissionTemplate)模型
|
||||||
|
- 字段包括:id, template_name, business_type, card_type, commission_type, commission_mode, commission_value, unfreeze_days, min_activation_for_unfreeze, approval_type, created_at, updated_at
|
||||||
|
- [x] 2.4.8 定义号卡运营商结算(CarrierSettlement)模型
|
||||||
|
- 字段包括:id, commission_record_id, agent_id, settlement_month, settlement_amount, status, created_at, updated_at
|
||||||
|
|
||||||
|
### 2.5 财务管理模型
|
||||||
|
- [x] 2.5.1 定义佣金提现申请(CommissionWithdrawalRequest)模型
|
||||||
|
- 字段包括:id, agent_id, amount, withdrawal_method, merchant_id, account_info, status, approved_by, approved_at, rejected_reason, paid_at, created_at, updated_at
|
||||||
|
- [x] 2.5.2 定义佣金提现设置(CommissionWithdrawalSetting)模型
|
||||||
|
- 字段包括:id, min_withdrawal_amount, max_withdrawal_amount, daily_withdrawal_limit, fee_rate, status, created_at, updated_at
|
||||||
|
- [x] 2.5.3 定义收款商户设置(PaymentMerchantSetting)模型
|
||||||
|
- 字段包括:id, user_id, merchant_type, account_name, account_number, bank_name, is_verified, is_default, status, created_at, updated_at
|
||||||
|
|
||||||
|
### 2.6 系统管理模型
|
||||||
|
- [x] 2.6.1 定义开发能力配置(DevCapabilityConfig)模型
|
||||||
|
- 字段包括:id, user_id, app_id, app_secret, callback_url, status, created_at, updated_at
|
||||||
|
- [x] 2.6.2 定义换卡申请(CardReplacementRequest)模型
|
||||||
|
- 字段包括:id, user_id, old_iccid, new_iccid, reason, status, processed_by, processed_at, created_at, updated_at
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 常量定义
|
||||||
|
|
||||||
|
### 3.1 核心业务常量
|
||||||
|
- [x] 3.1.1 在 `pkg/constants/iot.go` 中定义以下常量:
|
||||||
|
- IoT 卡状态:IotCardStatusInStock(1), IotCardStatusDistributed(2), IotCardStatusActivated(3), IotCardStatusSuspended(4)
|
||||||
|
- 设备状态:DeviceStatusInStock(1), DeviceStatusDistributed(2), DeviceStatusActivated(3), DeviceStatusSuspended(4)
|
||||||
|
- 号卡状态:NumberCardStatusOnSale(1), NumberCardStatusOffSale(2)
|
||||||
|
- IoT 卡激活状态:ActivationStatusInactive(0), ActivationStatusActive(1)
|
||||||
|
- IoT 卡实名状态:RealNameStatusNotVerified(0), RealNameStatusVerified(1)
|
||||||
|
- IoT 卡网络状态:NetworkStatusOffline(0), NetworkStatusOnline(1)
|
||||||
|
- 套餐流量类型:DataTypeReal("real"), DataTypeVirtual("virtual")
|
||||||
|
- 套餐类型:PackageTypeFormal("formal"), PackageTypeAddon("addon")
|
||||||
|
- 订单类型:OrderTypePackage(1), OrderTypeNumberCard(2)
|
||||||
|
- 订单状态:OrderStatusPending(1), OrderStatusPaid(2), OrderStatusCompleted(3), OrderStatusCancelled(4), OrderStatusRefunded(5)
|
||||||
|
- 支付方式:PaymentMethodWallet("wallet"), PaymentMethodOnline("online"), PaymentMethodCarrier("carrier")
|
||||||
|
- 所有者类型:OwnerTypePlatform("platform"), OwnerTypeAgent("agent"), OwnerTypeUser("user"), OwnerTypeDevice("device")
|
||||||
|
- 绑定状态:BindStatusBound(1), BindStatusUnbound(2)
|
||||||
|
|
||||||
|
### 3.2 套餐和轮询相关常量
|
||||||
|
- [x] 3.2.1 定义套餐使用类型常量:
|
||||||
|
- PackageUsageTypeSingleCard("single_card") - 单卡套餐
|
||||||
|
- PackageUsageTypeDevice("device") - 设备级套餐
|
||||||
|
- [x] 3.2.2 定义套餐使用状态常量:
|
||||||
|
- PackageUsageStatusActive(1) - 生效中
|
||||||
|
- PackageUsageStatusExhausted(2) - 已用完
|
||||||
|
- PackageUsageStatusExpired(3) - 已过期
|
||||||
|
- [x] 3.2.3 定义轮询配置卡条件常量:
|
||||||
|
- CardConditionNotRealName("not_real_name") - 未实名
|
||||||
|
- CardConditionRealName("real_name") - 已实名
|
||||||
|
- CardConditionActivated("activated") - 已激活
|
||||||
|
- CardConditionSuspended("suspended") - 已停用
|
||||||
|
- [x] 3.2.4 定义流量使用记录来源常量:
|
||||||
|
- DataUsageSourcePolling("polling") - 轮询
|
||||||
|
- DataUsageSourceManual("manual") - 手动
|
||||||
|
- DataUsageSourceGateway("gateway") - Gateway 回调
|
||||||
|
|
||||||
|
### 3.3 分佣相关常量
|
||||||
|
- [x] 3.3.1 定义分佣相关常量:
|
||||||
|
- 分佣类型:CommissionTypeOneTime("one_time"), CommissionTypeLongTerm("long_term"), CommissionTypeCombined("combined")
|
||||||
|
- 分佣模式:CommissionModeFixed("fixed"), CommissionModePercent("percent")
|
||||||
|
- 分佣状态:CommissionStatusFrozen(1), CommissionStatusUnfreezing(2), CommissionStatusReleased(3), CommissionStatusInvalid(4)
|
||||||
|
- 阶梯类型:LadderTypeActivation("activation"), LadderTypePickup("pickup"), LadderTypeDeposit("deposit")
|
||||||
|
- 卡类型:CardTypeNumberCard("number_card"), CardTypeIotCard("iot_card")
|
||||||
|
- 审批类型:ApprovalTypeAuto("auto"), ApprovalTypeManual("manual")
|
||||||
|
- 审批状态:ApprovalStatusPending(1), ApprovalStatusApproved(2), ApprovalStatusRejected(3)
|
||||||
|
|
||||||
|
### 3.4 财务管理常量
|
||||||
|
- [x] 3.4.1 定义财务相关常量:
|
||||||
|
- 提现状态:WithdrawalStatusPending(1), WithdrawalStatusApproved(2), WithdrawalStatusRejected(3), WithdrawalStatusPaid(4)
|
||||||
|
- 提现方式:WithdrawalMethodAlipay("alipay"), WithdrawalMethodWechat("wechat"), WithdrawalMethodBank("bank")
|
||||||
|
- 商户类型:MerchantTypeAlipay("alipay"), MerchantTypeWechat("wechat"), MerchantTypeBank("bank")
|
||||||
|
|
||||||
|
### 3.5 系统管理常量
|
||||||
|
- [x] 3.5.1 定义系统管理常量:
|
||||||
|
- 换卡申请状态:ReplacementStatusPending(1), ReplacementStatusApproved(2), ReplacementStatusRejected(3), ReplacementStatusCompleted(4)
|
||||||
|
- 开发能力配置状态:DevCapabilityStatusEnabled(1), DevCapabilityStatusDisabled(2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 模型和表结构文档
|
||||||
|
|
||||||
|
### 4.1 代码注释
|
||||||
|
- [x] 4.1.1 为所有 GORM 模型添加中文结构体注释(描述表的业务用途)
|
||||||
|
- [x] 4.1.2 为所有模型字段添加清晰的中文注释
|
||||||
|
- [x] 4.1.3 为所有常量添加中文注释(说明枚举值含义)
|
||||||
|
- [x] 4.1.4 在迁移脚本中为所有表和字段添加 SQL COMMENT
|
||||||
|
|
||||||
|
### 4.2 数据库设计文档
|
||||||
|
- [x] 4.2.1 在 `docs/iot-sim-management/` 目录下创建 `数据库设计.md`
|
||||||
|
- [x] 4.2.2 使用 Markdown 表格描述所有表结构(字段名、类型、约束、说明)
|
||||||
|
- [x] 4.2.3 使用 dbdiagram.io 或 draw.io 创建数据库 ERD 图
|
||||||
|
- [x] 4.2.4 导出 ERD 图并保存到 `docs/iot-sim-management/erd.png`
|
||||||
|
- [x] 4.2.5 在 `数据库设计.md` 中嵌入 ERD 图
|
||||||
|
|
||||||
|
### 4.3 模型使用说明
|
||||||
|
- [x] 4.3.1 创建 `docs/iot-sim-management/模型说明.md`
|
||||||
|
- [x] 4.3.2 说明每个模型的用途和关键字段含义
|
||||||
|
- [x] 4.3.3 说明表之间的关联关系(虽然没有外键,但逻辑关联需要说明)
|
||||||
|
- [x] 4.3.4 说明关键枚举字段的取值和含义
|
||||||
|
- [x] 4.3.5 说明特殊设计决策(如无外键约束、owner_type/owner_id 模式等)
|
||||||
|
|
||||||
|
### 4.4 轮询机制说明文档
|
||||||
|
- [x] 4.4.1 创建 `docs/iot-sim-management/轮询机制说明.md`
|
||||||
|
- [x] 4.4.2 说明三个独立轮询流程:实名状态轮询、卡流量轮询、套餐流量检查
|
||||||
|
- [x] 4.4.3 说明轮询配置的匹配规则和优先级
|
||||||
|
- [x] 4.4.4 说明 `enable_polling` 字段的使用场景
|
||||||
|
- [x] 4.4.5 说明流量使用记录表的数据保留策略
|
||||||
|
|
||||||
|
### 4.5 项目文档更新
|
||||||
|
- [x] 4.5.1 更新 `README.md`,添加 IoT SIM 管理模块描述
|
||||||
|
- [x] 4.5.2 在 README 中添加数据库设计文档链接
|
||||||
|
- [x] 4.5.3 在 README 中添加模型说明文档链接
|
||||||
|
- [x] 4.5.4 在 README 中添加轮询机制说明文档链接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 数据迁移验证
|
||||||
|
|
||||||
|
### 5.1 本地验证
|
||||||
|
- [x] 5.1.1 在本地 PostgreSQL 测试数据库执行迁移脚本 up
|
||||||
|
- [x] 5.1.2 使用 `\dt` 和 `\d table_name` 验证所有表创建成功
|
||||||
|
- [x] 5.1.3 验证所有字段类型、默认值、NOT NULL 约束正确
|
||||||
|
- [x] 5.1.4 使用 `\di` 验证所有索引创建成功
|
||||||
|
- [x] 5.1.5 验证唯一索引和组合索引的正确性
|
||||||
|
|
||||||
|
### 5.2 数据完整性验证
|
||||||
|
- [x] 5.2.1 插入测试数据验证唯一索引生效(尝试插入重复 ICCID 应失败)
|
||||||
|
- [x] 5.2.2 插入测试数据验证 NOT NULL 约束生效
|
||||||
|
- [x] 5.2.3 插入测试数据验证 CHECK 约束生效(如金额 >= 0)
|
||||||
|
- [x] 5.2.4 查询测试数据验证组合索引生效(使用 EXPLAIN ANALYZE)
|
||||||
|
- [x] 5.2.5 验证运营商初始数据插入成功
|
||||||
|
|
||||||
|
### 5.3 回滚验证
|
||||||
|
- [x] 5.3.1 执行迁移脚本 down
|
||||||
|
- [x] 5.3.2 验证所有表和索引删除成功
|
||||||
|
- [x] 5.3.3 重新执行 up 验证迁移脚本可重复执行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 代码质量检查
|
||||||
|
|
||||||
|
### 6.1 代码格式化
|
||||||
|
- [x] 6.1.1 使用 `go fmt` 格式化所有模型代码
|
||||||
|
- [x] 6.1.2 使用 `goimports` 整理导入语句
|
||||||
|
- [x] 6.1.3 使用 `golangci-lint` 检查代码质量
|
||||||
|
|
||||||
|
### 6.2 命名规范检查
|
||||||
|
- [x] 6.2.1 验证所有 Go 字段名遵循驼峰命名法(PascalCase)
|
||||||
|
- [x] 6.2.2 验证所有数据库字段名遵循下划线命名法(snake_case)
|
||||||
|
- [x] 6.2.3 验证所有常量命名遵循 Go 规范(如 IotCardStatusInStock)
|
||||||
|
- [x] 6.2.4 验证所有模型文件名遵循 Go 规范(snake_case,如 iot_card.go)
|
||||||
|
|
||||||
|
### 6.3 GORM 标签检查
|
||||||
|
- [x] 6.3.1 验证所有字段都有 `gorm:"column:字段名"` 标签
|
||||||
|
- [x] 6.3.2 验证所有字段都有 `json:"字段名"` 标签
|
||||||
|
- [x] 6.3.3 验证所有字段的 `comment` 标签包含中文说明
|
||||||
|
- [x] 6.3.4 验证字符串字段的 `type` 标签指定了长度(如 `type:varchar(100)`)
|
||||||
|
- [x] 6.3.5 验证数值字段的 `type` 标签指定了精度(如 `type:decimal(10,2)`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完成标准
|
||||||
|
|
||||||
|
本阶段任务完成后,应该具备:
|
||||||
|
|
||||||
|
1. ✅ 完整的数据库迁移脚本(up 和 down)
|
||||||
|
2. ✅ 完整的 GORM 模型定义(所有表对应的 Go 结构体)
|
||||||
|
3. ✅ 完整的常量定义(所有枚举值)
|
||||||
|
4. ✅ 完整的数据库设计文档(ERD 图 + 表结构说明)
|
||||||
|
5. ✅ 完整的模型使用说明文档
|
||||||
|
6. ✅ 完整的轮询机制说明文档
|
||||||
|
7. ✅ 数据库迁移在本地测试通过
|
||||||
|
8. ✅ 所有代码遵循项目开发规范(命名、注释、格式)
|
||||||
|
|
||||||
|
**不包含**:业务逻辑实现、API 接口、Service 层、Store 层、DTO、错误码、Redis Key 等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键设计说明
|
||||||
|
|
||||||
|
### 运营商表 (carriers)
|
||||||
|
- 存储运营商基础信息(中国移动、中国联通、中国电信)
|
||||||
|
- IoT 卡表通过 `carrier_id` 关联运营商表
|
||||||
|
|
||||||
|
### IoT 卡表 (iot_cards)
|
||||||
|
- `card_category` 字段:枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
|
||||||
|
- **普通卡**: 需要实名认证才能激活使用
|
||||||
|
- **行业卡**: 不需要实名认证,可以在 `real_name_status` 为 0 的情况下激活使用
|
||||||
|
- `carrier_id` 关联运营商表(替代原来的 carrier 字符串字段)
|
||||||
|
- `enable_polling` 控制是否参与轮询(默认 true,可手动禁用)
|
||||||
|
- `last_data_check_at` 记录卡流量检查时间
|
||||||
|
- `last_real_name_check_at` 记录实名检查时间
|
||||||
|
|
||||||
|
### 套餐使用情况表 (package_usages)
|
||||||
|
- 核心业务表,跟踪套餐的激活、使用、过期情况
|
||||||
|
- 单卡套餐:`usage_type` = "single_card",`iot_card_id` 有值
|
||||||
|
- 设备级套餐:`usage_type` = "device",`device_id` 有值
|
||||||
|
- `data_usage_mb` 通过汇总卡的流量计算(不是实时轮询,而是定期统计)
|
||||||
|
|
||||||
|
### 轮询配置表 (polling_configs)
|
||||||
|
- 支持梯度配置(未实名卡、实名卡使用不同的轮询策略)
|
||||||
|
- 支持按运营商配置不同的轮询频率
|
||||||
|
- 独立配置三种轮询:实名检查、卡流量检查、套餐流量检查
|
||||||
|
- `priority` 数字越小优先级越高
|
||||||
|
|
||||||
|
### 流量使用记录表 (data_usage_records)
|
||||||
|
- 记录每次卡流量检查的结果
|
||||||
|
- 支持按卡、按时间范围查询流量历史
|
||||||
|
- 数据量会快速增长,建议定期清理 90 天前的记录或使用分区表
|
||||||
|
|
||||||
|
### 轮询逻辑(概念说明)
|
||||||
|
1. **卡流量轮询**:只轮询有生效套餐的卡,`enable_polling = true`
|
||||||
|
2. **套餐流量检查**:定期汇总卡的流量,判断套餐是否超额
|
||||||
|
3. **实名状态轮询**:定期检查卡的实名状态,实名后降低轮询频率
|
||||||
|
- **行业卡特殊处理**: 行业卡的实名状态检查应该被禁用或设置为低优先级
|
||||||
|
4. **三个流程独立运行**:互不干扰,通过轮询配置表动态控制
|
||||||
|
|
||||||
|
### 分佣解冻逻辑(概念说明)
|
||||||
|
1. **一次性分佣**: 普通卡需要实名认证后才能解冻;行业卡无需实名认证,只需满足激活和充值条件
|
||||||
|
2. **长期分佣**: 普通卡需要实名认证后才能开始长期分佣;行业卡无需实名认证,满足其他条件即可
|
||||||
|
3. **组合分佣**: 行业卡的时间点条件从激活时开始计算(不是实名时)
|
||||||
346
openspec/specs/iot-agent-commission/spec.md
Normal file
346
openspec/specs/iot-agent-commission/spec.md
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
# IoT Agent Commission Management
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Manage commission rules and records for IoT agents, supporting three commission types (one-time, long-term, combined), ladder commissions, commission freeze/unfreeze logic, approval workflows, and multi-level agent commission distribution.
|
||||||
|
|
||||||
|
This capability supports:
|
||||||
|
- Agent hierarchy (tree structure) management
|
||||||
|
- Three commission types: one-time, long-term, combined
|
||||||
|
- Commission rule configuration (series-based for one-time, package-based for long-term)
|
||||||
|
- Combined commission with OR-condition unfreezing (time point OR package cycle)
|
||||||
|
- Ladder commission based on activation/pickup/deposit thresholds
|
||||||
|
- Commission record lifecycle (frozen → unfreezing → released → invalid)
|
||||||
|
- Commission unfreeze conditions (activation + real-name + recharge for normal cards; no real-name required for industry cards)
|
||||||
|
- Commission approval workflow (auto or manual)
|
||||||
|
- Multi-level agent commission distribution
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 代理树形关系
|
||||||
|
|
||||||
|
系统 SHALL 管理代理的树形层级关系,每个代理只有一个上级代理。
|
||||||
|
|
||||||
|
**agent_hierarchies 表**:
|
||||||
|
- `id`: 代理关系 ID(主键,BIGINT)
|
||||||
|
- `agent_id`: 代理用户 ID(BIGINT,唯一)
|
||||||
|
- `parent_agent_id`: 上级代理用户 ID(BIGINT,可空,NULL 表示顶级代理)
|
||||||
|
- `level`: 代理层级(INT,1-顶级代理 2-二级代理 ...)
|
||||||
|
- `path`: 代理路径(VARCHAR(500),如 "1/5/12",用于快速获取整个代理链)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建顶级代理
|
||||||
|
|
||||||
|
- **WHEN** 平台创建顶级代理(用户 ID 为 101)
|
||||||
|
- **THEN** 系统创建代理关系记录,`agent_id` 为 101,`parent_agent_id` 为 NULL,`level` 为 1,`path` 为 "101"
|
||||||
|
|
||||||
|
#### Scenario: 创建下级代理
|
||||||
|
|
||||||
|
- **WHEN** 顶级代理(ID 为 101)创建下级代理(用户 ID 为 102)
|
||||||
|
- **THEN** 系统创建代理关系记录,`agent_id` 为 102,`parent_agent_id` 为 101,`level` 为 2,`path` 为 "101/102"
|
||||||
|
|
||||||
|
#### Scenario: 查询代理的整个上级链
|
||||||
|
|
||||||
|
- **WHEN** 查询代理(ID 为 103,路径为 "101/102/103")的上级链
|
||||||
|
- **THEN** 系统解析 `path` 字段,返回代理 101(顶级)、102(父级)、103(当前代理)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣规则配置
|
||||||
|
|
||||||
|
系统 SHALL 支持为代理配置分佣规则,包括一次性分佣、长期分佣和组合分佣。
|
||||||
|
|
||||||
|
**commission_rules 表**:
|
||||||
|
- `id`: 分佣规则 ID(主键,BIGINT)
|
||||||
|
- `agent_id`: 代理用户 ID(BIGINT)
|
||||||
|
- `business_type`: 业务类型(VARCHAR(20),"iot_card"-IoT卡 | "number_card"-号卡)
|
||||||
|
- `commission_type`: 分佣类型(VARCHAR(20),"one_time"-一次性 | "long_term"-长期 | "combined"-组合)
|
||||||
|
- `series_id`: 套餐系列 ID(BIGINT,可空,**仅一次性分佣使用**,关联 package_series 表)
|
||||||
|
- `package_id`: 套餐 ID(BIGINT,可空,**仅长期分佣使用**,关联 packages 表)
|
||||||
|
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed"-固定金额 | "percent"-百分比)
|
||||||
|
- `commission_value`: 分佣值(DECIMAL(10,4),固定金额或百分比值)
|
||||||
|
- `freeze_days`: 冻结天数(INT,分佣冻结天数,默认 7)
|
||||||
|
- `is_ladder`: 是否阶梯分佣(BOOLEAN,默认 false)
|
||||||
|
- `status`: 规则状态(INT,1-有效 2-无效)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
**字段使用规则**:
|
||||||
|
- **一次性分佣**: 使用 `series_id` 关联套餐系列,`package_id` 为 NULL
|
||||||
|
- **长期分佣**: 使用 `package_id` 关联具体套餐,`series_id` 为 NULL
|
||||||
|
- **组合分佣**: 需要创建两条规则记录,一条一次性(使用 `series_id`),一条长期(使用 `package_id`)
|
||||||
|
- **`series_id` 和 `package_id` 互斥**: 不能同时有值
|
||||||
|
|
||||||
|
#### Scenario: 配置一次性分佣规则
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)配置一次性分佣规则,套餐系列 ID 为 1(月套餐系列),固定金额 5.00 元
|
||||||
|
- **THEN** 系统创建分佣规则,`agent_id` 为 123,`commission_type` 为 "one_time",`series_id` 为 1,`package_id` 为 NULL,`commission_mode` 为 "fixed",`commission_value` 为 5.00
|
||||||
|
|
||||||
|
#### Scenario: 配置长期分佣规则
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)配置长期分佣规则,套餐 ID 为 3001,百分比 5%
|
||||||
|
- **THEN** 系统创建分佣规则,`agent_id` 为 123,`commission_type` 为 "long_term",`series_id` 为 NULL,`package_id` 为 3001,`commission_mode` 为 "percent",`commission_value` 为 0.05
|
||||||
|
|
||||||
|
#### Scenario: 配置组合分佣规则
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)配置组合分佣规则,套餐系列 ID 为 1,先一次性分佣 10.00 元,连续在网 3 个月后开始长期分佣(套餐 ID 为 3001)3.00 元/月
|
||||||
|
- **THEN** 系统创建两条分佣规则:
|
||||||
|
- 一条 `commission_type` 为 "one_time",`series_id` 为 1,`package_id` 为 NULL
|
||||||
|
- 另一条 `commission_type` 为 "long_term",`series_id` 为 NULL,`package_id` 为 3001,且关联组合条件
|
||||||
|
|
||||||
|
#### Scenario: 字段互斥校验
|
||||||
|
|
||||||
|
- **WHEN** 平台尝试创建分佣规则,同时设置 `series_id` 为 1 和 `package_id` 为 3001
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"`series_id` 和 `package_id` 不能同时有值"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 组合分佣条件配置
|
||||||
|
|
||||||
|
系统 SHALL 支持为组合分佣配置解冻条件,包括时间点条件和套餐周期条件。
|
||||||
|
|
||||||
|
**commission_combined_conditions 表**:
|
||||||
|
- `id`: 组合条件 ID(主键,BIGINT)
|
||||||
|
- `commission_rule_id`: 关联的分佣规则 ID(BIGINT,必须是 commission_type 为 "long_term" 且属于组合分佣的规则)
|
||||||
|
- `condition_type`: 条件类型(VARCHAR(20),"time_point"-时间点 | "package_cycle"-套餐周期)
|
||||||
|
- `time_months`: 时间月数(INT,可空,仅当 condition_type 为 "time_point" 时有值,表示实名后多少个月)
|
||||||
|
- `package_cycle_threshold`: 套餐周期阈值(INT,可空,仅当 condition_type 为 "package_cycle" 时有值,表示使用多少个套餐周期)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
**解冻逻辑**: 组合分佣的长期部分,当满足**任一条件**(OR 关系)时开始产生长期分佣。
|
||||||
|
|
||||||
|
#### Scenario: 配置时间点条件
|
||||||
|
|
||||||
|
- **WHEN** 平台为组合分佣规则(ID 为 501)配置时间点条件,实名后 3 个月开始长期分佣
|
||||||
|
- **THEN** 系统创建组合条件记录,`commission_rule_id` 为 501,`condition_type` 为 "time_point",`time_months` 为 3
|
||||||
|
|
||||||
|
#### Scenario: 配置套餐周期条件
|
||||||
|
|
||||||
|
- **WHEN** 平台为组合分佣规则(ID 为 501)配置套餐周期条件,使用 10 个套餐周期后开始长期分佣
|
||||||
|
- **THEN** 系统创建组合条件记录,`commission_rule_id` 为 501,`condition_type` 为 "package_cycle",`package_cycle_threshold` 为 10
|
||||||
|
|
||||||
|
#### Scenario: 同时配置两种条件(OR 关系)
|
||||||
|
|
||||||
|
- **WHEN** 平台为组合分佣规则(ID 为 501)同时配置时间点条件(6 个月)和套餐周期条件(10 个周期)
|
||||||
|
- **THEN** 系统创建两条组合条件记录,长期分佣在任一条件满足时开始
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 阶梯分佣配置
|
||||||
|
|
||||||
|
系统 SHALL 支持阶梯分佣,根据激活量/提货量达到阶梯条件后变更分佣值。
|
||||||
|
|
||||||
|
**commission_ladder 表**:
|
||||||
|
- `id`: 阶梯配置 ID(主键,BIGINT)
|
||||||
|
- `commission_rule_id`: 关联的分佣规则 ID(BIGINT)
|
||||||
|
- `ladder_type`: 阶梯类型(VARCHAR(20),"activation"-激活量 | "pickup"-提货量 | "deposit"-保证金)
|
||||||
|
- `ladder_threshold`: 阶梯阈值(INT,如激活 100 张)
|
||||||
|
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed"-固定金额 | "percent"-百分比)
|
||||||
|
- `commission_value`: 分佣值(DECIMAL(10,4),达到阶梯后的分佣值)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 配置激活量阶梯
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)配置阶梯分佣,激活 100 张卡后分佣从 5.00 元提升到 8.00 元
|
||||||
|
- **THEN** 系统创建阶梯配置,`ladder_type` 为 "activation",`ladder_threshold` 为 100,`commission_value` 为 8.00
|
||||||
|
|
||||||
|
#### Scenario: 计算阶梯分佣
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)当月激活量达到 100 张
|
||||||
|
- **THEN** 系统根据阶梯配置,从第 101 张卡开始使用新的分佣值 8.00 元
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣记录管理
|
||||||
|
|
||||||
|
系统 SHALL 记录每笔分佣,支持冻结、解冻和发放流程。
|
||||||
|
|
||||||
|
**commission_records 表**:
|
||||||
|
- `id`: 分佣记录 ID(主键,BIGINT)
|
||||||
|
- `agent_id`: 代理用户 ID(BIGINT)
|
||||||
|
- `order_id`: 订单 ID(BIGINT)
|
||||||
|
- `commission_rule_id`: 分佣规则 ID(BIGINT)
|
||||||
|
- `commission_type`: 分佣类型(VARCHAR(20),"one_time" | "long_term" | "combined")
|
||||||
|
- `amount`: 分佣金额(DECIMAL(10,2),元)
|
||||||
|
- `status`: 分佣状态(INT,1-冻结 2-解冻中 3-已发放 4-已失效)
|
||||||
|
- `freeze_until`: 冻结截止时间(TIMESTAMP,可空)
|
||||||
|
- `released_at`: 发放时间(TIMESTAMP,可空)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建一次性分佣记录
|
||||||
|
|
||||||
|
- **WHEN** 订单(ID 为 10001)完成,触发代理(ID 为 123)的一次性分佣 5.00 元,冻结 7 天
|
||||||
|
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结),`freeze_until` 为 7 天后
|
||||||
|
|
||||||
|
#### Scenario: 分佣自动解冻
|
||||||
|
|
||||||
|
- **WHEN** 分佣记录(ID 为 1001)的冻结截止时间到达,且满足解冻条件(激活+实名+充值)
|
||||||
|
- **THEN** 系统将分佣状态从 1(冻结) 变更为 2(解冻中),创建分佣解冻审批记录
|
||||||
|
|
||||||
|
#### Scenario: 分佣发放
|
||||||
|
|
||||||
|
- **WHEN** 分佣解冻审批通过
|
||||||
|
- **THEN** 系统将分佣状态从 2(解冻中) 变更为 3(已发放),将分佣金额转入代理钱包,`released_at` 记录发放时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣解冻条件
|
||||||
|
|
||||||
|
系统 SHALL 根据分佣类型校验不同的解冻条件。
|
||||||
|
|
||||||
|
**一次性分佣解冻条件**:
|
||||||
|
- 激活(实名状态为已实名;对于行业卡,实名状态可以为未实名)
|
||||||
|
- 达到累计/首次充值金额
|
||||||
|
- 冻结天数到达
|
||||||
|
|
||||||
|
**长期分佣解冻条件**:
|
||||||
|
- 激活(实名状态为已实名;对于行业卡,实名状态可以为未实名)
|
||||||
|
- 达到累计/首次充值金额
|
||||||
|
- 在网状态正常
|
||||||
|
- 三无校验通过(通过 Excel 导入解冻)
|
||||||
|
|
||||||
|
**组合分佣解冻条件**:
|
||||||
|
- **一次性部分**: 立即产生并按一次性分佣条件解冻
|
||||||
|
- **长期部分**: 当满足以下**任一条件**时开始长期分佣(OR 关系):
|
||||||
|
- 达到某个时间点之后(例如:实名后 3 个月)
|
||||||
|
- **OR** 该 IoT 卡的套餐使用周期数达到阈值(例如:10 个周期)
|
||||||
|
- **注意**: 套餐周期阈值是针对单张 IoT 卡的,不是设备级别
|
||||||
|
|
||||||
|
#### Scenario: 一次性分佣满足解冻条件
|
||||||
|
|
||||||
|
- **WHEN** 分佣记录(ID 为 1001)的冻结截止时间到达,用户已实名且已充值
|
||||||
|
- **THEN** 系统将分佣状态变更为 2(解冻中),创建审批记录
|
||||||
|
|
||||||
|
#### Scenario: 长期分佣等待 Excel 导入解冻
|
||||||
|
|
||||||
|
- **WHEN** 长期分佣记录等待三无校验
|
||||||
|
- **THEN** 系统保持分佣状态为 1(冻结),等待平台通过 Excel 导入解冻数据
|
||||||
|
|
||||||
|
#### Scenario: 组合分佣时间点条件满足
|
||||||
|
|
||||||
|
- **WHEN** 组合分佣规则配置为实名后 3 个月开始长期分佣,IoT 卡已实名 3 个月
|
||||||
|
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录,即使套餐周期数未达到阈值
|
||||||
|
|
||||||
|
#### Scenario: 组合分佣套餐周期条件满足
|
||||||
|
|
||||||
|
- **WHEN** 组合分佣规则配置为套餐使用 10 个周期后开始长期分佣,IoT 卡已使用套餐 10 个周期
|
||||||
|
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录,即使未达到时间点要求
|
||||||
|
|
||||||
|
#### Scenario: 组合分佣任一条件满足即开始
|
||||||
|
|
||||||
|
- **WHEN** 组合分佣规则配置为"实名后 6 个月 OR 10 个套餐周期",IoT 卡已使用 10 个周期但只实名 2 个月
|
||||||
|
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录(因为套餐周期条件已满足)
|
||||||
|
|
||||||
|
#### Scenario: 行业卡一次性分佣解冻(无需实名)
|
||||||
|
|
||||||
|
- **WHEN** 行业卡(card_category 为 "industry")的一次性分佣记录冻结期到达,卡已激活且已充值,但实名状态为未实名
|
||||||
|
- **THEN** 系统判定解冻条件满足(行业卡无需实名认证),将分佣状态变更为 2(解冻中),创建审批记录
|
||||||
|
|
||||||
|
#### Scenario: 行业卡长期分佣解冻(无需实名)
|
||||||
|
|
||||||
|
- **WHEN** 行业卡(card_category 为 "industry")的长期分佣记录满足充值金额和在网状态,但实名状态为未实名
|
||||||
|
- **THEN** 系统判定行业卡无需实名认证,等待三无校验通过后可解冻
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣解冻审批
|
||||||
|
|
||||||
|
系统 SHALL 支持分佣解冻审批流程,审批通过后发放分佣。
|
||||||
|
|
||||||
|
**commission_approvals 表**:
|
||||||
|
- `id`: 审批记录 ID(主键,BIGINT)
|
||||||
|
- `commission_record_id`: 分佣记录 ID(BIGINT)
|
||||||
|
- `approval_type`: 审批类型(VARCHAR(20),"auto"-自动 | "manual"-人工)
|
||||||
|
- `status`: 审批状态(INT,1-待审批 2-已通过 3-已拒绝)
|
||||||
|
- `approver_id`: 审批人用户 ID(BIGINT,可空)
|
||||||
|
- `approval_time`: 审批时间(TIMESTAMP,可空)
|
||||||
|
- `approval_note`: 审批备注(TEXT,可空)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建审批记录
|
||||||
|
|
||||||
|
- **WHEN** 分佣记录(ID 为 1001)状态变更为 2(解冻中)
|
||||||
|
- **THEN** 系统创建审批记录,`commission_record_id` 为 1001,`approval_type` 为 "auto",状态为 1(待审批)
|
||||||
|
|
||||||
|
#### Scenario: 审批通过
|
||||||
|
|
||||||
|
- **WHEN** 审批人(用户 ID 为 999)审批通过审批记录(ID 为 2001)
|
||||||
|
- **THEN** 系统将审批状态变更为 2(已通过),分佣记录状态变更为 3(已发放),将分佣金额转入代理钱包
|
||||||
|
|
||||||
|
#### Scenario: 审批拒绝
|
||||||
|
|
||||||
|
- **WHEN** 审批人拒绝审批记录(ID 为 2001),备注"用户未满足在网条件"
|
||||||
|
- **THEN** 系统将审批状态变更为 3(已拒绝),分佣记录状态变更为 4(已失效)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣模板
|
||||||
|
|
||||||
|
系统 SHALL 支持创建分佣模板,存储常用的分佣方案,便于快速配置。
|
||||||
|
|
||||||
|
**commission_templates 表**:
|
||||||
|
- `id`: 模板 ID(主键,BIGINT)
|
||||||
|
- `template_name`: 模板名称(VARCHAR(255))
|
||||||
|
- `business_type`: 业务类型(VARCHAR(20),"iot_card"-IoT卡 | "number_card"-号卡)
|
||||||
|
- `commission_type`: 分佣类型(VARCHAR(20),"one_time" | "long_term" | "combined")
|
||||||
|
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed" | "percent")
|
||||||
|
- `commission_value`: 分佣值(DECIMAL(10,4))
|
||||||
|
- `freeze_days`: 冻结天数(INT)
|
||||||
|
- `is_ladder`: 是否阶梯分佣(BOOLEAN)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建分佣模板
|
||||||
|
|
||||||
|
- **WHEN** 平台创建分佣模板"标准月套餐分佣",业务类型为 IoT 卡,一次性分佣 5.00 元,冻结 7 天
|
||||||
|
- **THEN** 系统创建模板记录,`template_name` 为 "标准月套餐分佣",`business_type` 为 "iot_card",`commission_type` 为 "one_time",`commission_value` 为 5.00,`freeze_days` 为 7
|
||||||
|
|
||||||
|
#### Scenario: 应用分佣模板
|
||||||
|
|
||||||
|
- **WHEN** 平台为代理(ID 为 123)应用模板(ID 为 501)
|
||||||
|
- **THEN** 系统根据模板配置创建分佣规则,`agent_id` 为 123,其他字段从模板复制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 多级代理分佣
|
||||||
|
|
||||||
|
系统 SHALL 支持多级代理分佣,根据代理路径计算每一级代理的分佣。
|
||||||
|
|
||||||
|
**多级分佣规则**:
|
||||||
|
- 通过代理路径(`path`)获取整个代理链
|
||||||
|
- 为每一级代理查找对应的分佣规则
|
||||||
|
- 创建多条分佣记录,每条对应一个代理
|
||||||
|
|
||||||
|
#### Scenario: 三级代理分佣
|
||||||
|
|
||||||
|
- **WHEN** 订单(ID 为 10001)的代理路径为 "101/102/103",每级代理配置分佣:101(2.00 元)、102(3.00 元)、103(5.00 元)
|
||||||
|
- **THEN** 系统创建 3 条分佣记录:代理 101 的 2.00 元、代理 102 的 3.00 元、代理 103 的 5.00 元
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 分佣数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对分佣数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 代理 ID(agent_id):必填,≥ 1
|
||||||
|
- 订单 ID(order_id):必填,≥ 1
|
||||||
|
- 分佣金额(amount):必填,≥ 0,最多 2 位小数
|
||||||
|
- 分佣状态(status):必填,枚举值 1-4
|
||||||
|
- 冻结天数(freeze_days):必填,≥ 0
|
||||||
|
|
||||||
|
#### Scenario: 创建分佣记录时金额为负数
|
||||||
|
|
||||||
|
- **WHEN** 创建分佣记录,金额为 -5.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"分佣金额必须 ≥ 0"
|
||||||
|
|
||||||
|
#### Scenario: 创建分佣规则时分佣值无效
|
||||||
|
|
||||||
|
- **WHEN** 创建分佣规则,分佣模式为百分比,分佣值为 1.5(超过 100%)
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"百分比分佣值必须在 0-1 之间"
|
||||||
304
openspec/specs/iot-card/spec.md
Normal file
304
openspec/specs/iot-card/spec.md
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
# IoT Card Management
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Manage IoT cards (SIM cards) for the IoT management system, including inventory management, distribution, activation, status tracking, and Gateway integration.
|
||||||
|
|
||||||
|
This capability supports:
|
||||||
|
- IoT card entity definition and lifecycle management
|
||||||
|
- Platform self-operation and agent distribution models
|
||||||
|
- Integration with Gateway project for real-time status synchronization
|
||||||
|
- Batch import and multi-dimensional querying
|
||||||
|
- Support for normal cards (require real-name verification) and industry cards (no real-name required)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: IoT 卡实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义 IoT 卡(IotCard)实体,包含 IoT 卡(物联网卡/流量卡/SIM卡)的商品属性、状态属性、所有权信息和 Gateway 集成字段。
|
||||||
|
|
||||||
|
**核心概念**: IoT 卡 = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)。系统使用 ICCID 作为 IoT 卡的唯一标识。
|
||||||
|
|
||||||
|
**卡业务类型**:
|
||||||
|
- **普通卡(normal)**: 需要实名认证才能激活使用,遵循运营商实名制要求
|
||||||
|
- **行业卡(industry)**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
|
||||||
|
**商品属性**:
|
||||||
|
- `id`: IoT 卡 ID(主键,BIGINT)
|
||||||
|
- `iccid`: ICCID(VARCHAR(50),唯一,国际移动用户识别码,IoT卡的唯一标识)
|
||||||
|
- `card_type`: 卡类型(VARCHAR(50),如 "4G"、"5G"、"NB-IoT")
|
||||||
|
- `card_category`: 卡业务类型(VARCHAR(20),枚举值:"normal"-普通卡 | "industry"-行业卡,默认 "normal")
|
||||||
|
- `carrier_id`: 运营商 ID(BIGINT,关联 carriers 表,如中国移动、中国联通、中国电信)
|
||||||
|
- `imsi`: IMSI(VARCHAR(50),可选,国际移动用户识别码)
|
||||||
|
- `msisdn`: 手机号码(VARCHAR(20),可选)
|
||||||
|
- `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯)
|
||||||
|
- `supplier`: 供应商名称(VARCHAR(255),可选)
|
||||||
|
- `cost_price`: 成本价(DECIMAL(10,2),平台进货价)
|
||||||
|
- `distribute_price`: 分销价(DECIMAL(10,2),分销给代理的价格,仅当 owner_type 为 agent 时有值)
|
||||||
|
|
||||||
|
**所有权和状态**:
|
||||||
|
- `status`: IoT 卡状态(INT,1-在库 2-已分销 3-已激活 4-已停用)
|
||||||
|
- `owner_type`: 所有者类型(VARCHAR(20),"platform"-平台自营 | "agent"-代理商 | "user"-用户 | "device"-设备)
|
||||||
|
- `owner_id`: 所有者 ID(BIGINT,platform 时为 0,agent/user/device 时为对应的 ID)
|
||||||
|
- `activated_at`: 激活时间(TIMESTAMP,可空)
|
||||||
|
|
||||||
|
**Gateway 集成字段**(从 Gateway 项目同步):
|
||||||
|
- `activation_status`: 激活状态(INT,0-未激活 1-已激活)
|
||||||
|
- `real_name_status`: 实名状态(INT,0-未实名 1-已实名)
|
||||||
|
- `network_status`: 网络状态(INT,0-停机 1-开机)
|
||||||
|
- `data_usage_mb`: 累计流量使用(BIGINT,MB 为单位,默认 0)
|
||||||
|
- `last_sync_time`: 最后一次与 Gateway 同步时间(TIMESTAMP,可空)
|
||||||
|
|
||||||
|
**轮询控制字段**:
|
||||||
|
- `enable_polling`: 是否参与轮询(BOOLEAN,默认 true,用于控制是否对该卡进行定时轮询)
|
||||||
|
- `last_data_check_at`: 最后一次卡流量检查时间(TIMESTAMP,可空,记录上次轮询卡流量的时间)
|
||||||
|
- `last_real_name_check_at`: 最后一次实名检查时间(TIMESTAMP,可空,记录上次轮询实名状态的时间)
|
||||||
|
|
||||||
|
**系统字段**:
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建平台自营 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 平台批量导入 IoT 卡数据,ICCID 为 "89860123456789012345"
|
||||||
|
- **THEN** 系统创建 IoT 卡记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(在库),`activation_status` 为 0(未激活)
|
||||||
|
|
||||||
|
#### Scenario: 平台分销 IoT 卡给代理
|
||||||
|
|
||||||
|
- **WHEN** 平台将在库 IoT 卡分销给代理商(用户 ID 为 123),设置分销价为 50.00 元
|
||||||
|
- **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 2(已分销),`owner_type` 变更为 "agent",`owner_id` 设置为 123,`distribute_price` 设置为 50.00
|
||||||
|
|
||||||
|
#### Scenario: IoT 卡绑定到设备
|
||||||
|
|
||||||
|
- **WHEN** 用户将 IoT 卡(ICCID 为 "8986...")绑定到设备(ID 为 1001)
|
||||||
|
- **THEN** 系统在 `device_sim_bindings` 表创建绑定记录,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为 1001
|
||||||
|
|
||||||
|
#### Scenario: IoT 卡直接销售给用户
|
||||||
|
|
||||||
|
- **WHEN** 平台或代理将 IoT 卡直接销售给用户(用户 ID 为 2001)
|
||||||
|
- **THEN** 系统创建套餐订单记录,IoT 卡的 `owner_type` 变更为 "user",`owner_id` 变更为 2001
|
||||||
|
|
||||||
|
#### Scenario: 行业卡无需实名认证
|
||||||
|
|
||||||
|
- **WHEN** 创建卡业务类型为 "industry"(行业卡)的 IoT 卡
|
||||||
|
- **THEN** 系统允许该卡在 `real_name_status` 为 0(未实名)的情况下激活使用,不强制要求实名认证
|
||||||
|
|
||||||
|
#### Scenario: 普通卡需要实名认证
|
||||||
|
|
||||||
|
- **WHEN** 创建卡业务类型为 "normal"(普通卡)的 IoT 卡
|
||||||
|
- **THEN** 系统要求该卡必须先完成实名认证(`real_name_status` 为 1)才能激活使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡状态流转
|
||||||
|
|
||||||
|
系统 SHALL 管理 IoT 卡的状态流转,确保状态变更符合业务规则。
|
||||||
|
|
||||||
|
**状态定义**:
|
||||||
|
- **1-在库**: IoT 卡在平台库存中,未分销
|
||||||
|
- **2-已分销**: IoT 卡已分销给代理商,代理可销售
|
||||||
|
- **3-已激活**: IoT 卡已被终端用户激活使用
|
||||||
|
- **4-已停用**: IoT 卡已停用,不可使用
|
||||||
|
|
||||||
|
**状态流转规则**:
|
||||||
|
- 在库(1) → 已分销(2): 平台分销给代理
|
||||||
|
- 在库(1) → 已激活(3): 平台自营直接销售给用户并激活
|
||||||
|
- 已分销(2) → 已激活(3): 代理销售给用户并激活
|
||||||
|
- 已激活(3) → 已停用(4): 用户或平台主动停用
|
||||||
|
- 已停用(4) → 已激活(3): 用户或平台主动复机(仅在符合业务规则时)
|
||||||
|
|
||||||
|
#### Scenario: 代理销售 IoT 卡给用户
|
||||||
|
|
||||||
|
- **WHEN** 代理商销售已分销 IoT 卡给终端用户并激活
|
||||||
|
- **THEN** 系统将 IoT 卡状态从 2(已分销) 变更为 3(已激活),`activated_at` 记录激活时间,`activation_status` 从 Gateway 同步后变更为 1
|
||||||
|
|
||||||
|
#### Scenario: 平台自营销售 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 平台直接销售在库 IoT 卡给终端用户并激活
|
||||||
|
- **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 3(已激活),`owner_type` 保持 "platform",`activated_at` 记录激活时间
|
||||||
|
|
||||||
|
#### Scenario: 停用已激活 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 用户或平台停用已激活 IoT 卡
|
||||||
|
- **THEN** 系统将 IoT 卡状态从 3(已激活) 变更为 4(已停用),通过 Gateway API 执行停机操作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡平台自营和代理分销
|
||||||
|
|
||||||
|
系统 SHALL 支持 IoT 卡的平台自营销售和代理分销两种模式,通过 `owner_type` 和 `owner_id` 区分所有者。
|
||||||
|
|
||||||
|
**平台自营**:
|
||||||
|
- `owner_type` 为 "platform"
|
||||||
|
- `owner_id` 为 0
|
||||||
|
- 平台直接销售给终端用户
|
||||||
|
- 销售价格由平台自主定价
|
||||||
|
|
||||||
|
**代理分销**:
|
||||||
|
- `owner_type` 为 "agent"
|
||||||
|
- `owner_id` 为代理用户 ID
|
||||||
|
- 代理商可以销售给终端用户或下级代理
|
||||||
|
- 分销价格由平台设置(`distribute_price`),代理商可在分销价基础上加价(但不能超过 2 倍)
|
||||||
|
|
||||||
|
#### Scenario: 查询平台自营 IoT 卡库存
|
||||||
|
|
||||||
|
- **WHEN** 查询平台自营 IoT 卡库存
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "platform" 且 `status` 为 1(在库) 的 IoT 卡列表
|
||||||
|
|
||||||
|
#### Scenario: 查询代理分销 IoT 卡库存
|
||||||
|
|
||||||
|
- **WHEN** 代理商(用户 ID 为 123)查询自己的 IoT 卡库存
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 且 `status` 为 2(已分销) 的 IoT 卡列表
|
||||||
|
|
||||||
|
#### Scenario: 代理加价销售 IoT 卡套餐
|
||||||
|
|
||||||
|
- **WHEN** 代理商为已分销 IoT 卡设置套餐售价
|
||||||
|
- **THEN** 系统校验套餐售价不超过分销价的 2 倍,校验通过后允许销售
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡批量导入
|
||||||
|
|
||||||
|
系统 SHALL 支持批量导入 IoT 卡数据,用于初始化库存或补充库存。
|
||||||
|
|
||||||
|
**导入字段**:
|
||||||
|
- ICCID(必填)
|
||||||
|
- 卡类型(必填,如 "4G"、"5G"、"NB-IoT")
|
||||||
|
- 卡业务类型(可选,枚举值 "normal" | "industry",默认 "normal")
|
||||||
|
- 运营商 ID(必填,从 carriers 表中选择)
|
||||||
|
- IMSI(可选)
|
||||||
|
- 手机号码(可选)
|
||||||
|
- 供应商(可选)
|
||||||
|
- 成本价(必填)
|
||||||
|
- 批次号(必填)
|
||||||
|
|
||||||
|
**导入规则**:
|
||||||
|
- ICCID 必须唯一,重复 ICCID 将被拒绝
|
||||||
|
- 导入的 IoT 卡默认状态为 1(在库),所有者为平台(`owner_type` 为 "platform",`owner_id` 为 0)
|
||||||
|
- 导入成功后记录操作日志
|
||||||
|
|
||||||
|
#### Scenario: 批量导入 IoT 卡成功
|
||||||
|
|
||||||
|
- **WHEN** 平台上传包含 100 条 IoT 卡数据的 CSV 文件
|
||||||
|
- **THEN** 系统创建 100 条 IoT 卡记录,状态为 1(在库),所有者为平台,返回导入成功消息
|
||||||
|
|
||||||
|
#### Scenario: 批量导入包含重复 ICCID
|
||||||
|
|
||||||
|
- **WHEN** 平台上传的 CSV 文件中包含已存在的 ICCID
|
||||||
|
- **THEN** 系统拒绝重复 ICCID 的 IoT 卡,返回错误信息并列出重复 ICCID,其他有效 IoT 卡正常导入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡查询和筛选
|
||||||
|
|
||||||
|
系统 SHALL 支持多维度查询和筛选 IoT 卡,包括状态、所有者、批次号、卡类型等。
|
||||||
|
|
||||||
|
**查询条件**:
|
||||||
|
- ICCID(精确匹配或模糊匹配)
|
||||||
|
- IoT 卡状态(单选或多选)
|
||||||
|
- 所有者类型(platform | agent | user | device)
|
||||||
|
- 所有者 ID(仅当所有者类型为 agent/user/device 时有效)
|
||||||
|
- 批次号(精确匹配)
|
||||||
|
- 卡类型(单选或多选)
|
||||||
|
- 运营商 ID(单选或多选,从 carriers 表选择)
|
||||||
|
- 激活状态(0-未激活 | 1-已激活)
|
||||||
|
- 实名状态(0-未实名 | 1-已实名)
|
||||||
|
- 网络状态(0-停机 | 1-开机)
|
||||||
|
- 是否参与轮询(true | false)
|
||||||
|
- 激活时间范围(开始时间 - 结束时间)
|
||||||
|
- 创建时间范围(开始时间 - 结束时间)
|
||||||
|
|
||||||
|
**分页**:
|
||||||
|
- 默认每页 20 条,最大每页 100 条
|
||||||
|
- 返回总记录数和总页数
|
||||||
|
|
||||||
|
#### Scenario: 查询特定批次的在库 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 平台查询批次号为 "BATCH-2025-001" 且状态为 1(在库) 的 IoT 卡
|
||||||
|
- **THEN** 系统返回符合条件的 IoT 卡列表,包含 ICCID、类型、运营商、成本价等信息
|
||||||
|
|
||||||
|
#### Scenario: 代理查询自己的已分销 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 代理商(用户 ID 为 123)查询自己的已分销 IoT 卡
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 且 `status` 为 2(已分销) 的 IoT 卡列表
|
||||||
|
|
||||||
|
#### Scenario: 分页查询 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 平台查询在库 IoT 卡,指定每页 50 条,查询第 2 页
|
||||||
|
- **THEN** 系统返回第 51-100 条 IoT 卡记录,以及总记录数和总页数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Gateway 集成
|
||||||
|
|
||||||
|
系统 SHALL 预留 IoT 卡状态相关字段,用于后续与 Gateway 项目集成。
|
||||||
|
|
||||||
|
**集成字段**:
|
||||||
|
- `activation_status`: 激活状态(从 Gateway 同步)
|
||||||
|
- `real_name_status`: 实名状态(从 Gateway 同步)
|
||||||
|
- `network_status`: 网络状态(从 Gateway 同步)
|
||||||
|
- `data_usage_mb`: 累计流量使用(从 Gateway 同步)
|
||||||
|
- `last_sync_time`: 最后同步时间
|
||||||
|
|
||||||
|
**集成说明**:
|
||||||
|
- 本阶段只设计数据模型字段,不实现 Gateway HTTP 客户端代码
|
||||||
|
- 后续 Service 层将调用 Gateway API 获取 IoT 卡状态并更新这些字段
|
||||||
|
- Gateway 使用 AES 加密 + MD5 签名的统一传输协议(参考 design.md)
|
||||||
|
|
||||||
|
**Gateway API 功能**:
|
||||||
|
- 查询 IoT 卡状态(激活状态、实名状态、网络状态)
|
||||||
|
- 查询流量详情(累计流量使用、剩余流量)
|
||||||
|
- 停复机操作(停机、复机)
|
||||||
|
- 实名认证操作
|
||||||
|
|
||||||
|
#### Scenario: 预留 Gateway 集成字段
|
||||||
|
|
||||||
|
- **WHEN** 创建 IoT 卡记录
|
||||||
|
- **THEN** 系统初始化 Gateway 相关字段为默认值:`activation_status` 为 0,`real_name_status` 为 0,`network_status` 为 0,`data_usage_mb` 为 0,`last_sync_time` 为空
|
||||||
|
|
||||||
|
#### Scenario: 从 Gateway 同步 IoT 卡状态
|
||||||
|
|
||||||
|
- **WHEN** Service 层调用 Gateway API 查询 IoT 卡状态
|
||||||
|
- **THEN** 系统更新 IoT 卡的 `activation_status`、`real_name_status`、`network_status`、`data_usage_mb` 和 `last_sync_time` 字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: IoT 卡数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对 IoT 卡数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- ICCID(iccid):必填,长度 19-20 字符,唯一
|
||||||
|
- 卡类型(card_type):必填,长度 1-50 字符
|
||||||
|
- 卡业务类型(card_category):必填,枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
|
||||||
|
- 运营商 ID(carrier_id):必填,≥ 1,必须是有效的运营商 ID
|
||||||
|
- 成本价(cost_price):必填,≥ 0,最多 2 位小数
|
||||||
|
- 分销价(distribute_price):可选,≥ 0,最多 2 位小数,≥ 成本价
|
||||||
|
- 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user" | "device"
|
||||||
|
- 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
|
||||||
|
- 激活状态(activation_status):必填,枚举值 0(未激活) | 1(已激活)
|
||||||
|
- 实名状态(real_name_status):必填,枚举值 0(未实名) | 1(已实名),当 card_category 为 "industry"(行业卡)时可以保持 0
|
||||||
|
- 网络状态(network_status):必填,枚举值 0(停机) | 1(开机)
|
||||||
|
- 轮询开关(enable_polling):必填,布尔值 true | false
|
||||||
|
|
||||||
|
#### Scenario: 创建 IoT 卡时 ICCID 格式错误
|
||||||
|
|
||||||
|
- **WHEN** 平台创建 IoT 卡,ICCID 长度为 15(小于 19)
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"ICCID 长度必须为 19-20 字符"
|
||||||
|
|
||||||
|
#### Scenario: 创建 IoT 卡时 ICCID 重复
|
||||||
|
|
||||||
|
- **WHEN** 平台创建 IoT 卡,ICCID 为已存在的 "89860123456789012345"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"ICCID 已存在"
|
||||||
|
|
||||||
|
#### Scenario: 创建 IoT 卡时成本价为负数
|
||||||
|
|
||||||
|
- **WHEN** 平台创建 IoT 卡,成本价为 -10.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"成本价必须 ≥ 0"
|
||||||
|
|
||||||
|
#### Scenario: 创建 IoT 卡时分销价低于成本价
|
||||||
|
|
||||||
|
- **WHEN** 平台创建 IoT 卡,成本价为 50.00,分销价为 40.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"分销价不能低于成本价"
|
||||||
325
openspec/specs/iot-device/spec.md
Normal file
325
openspec/specs/iot-device/spec.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# IoT Device Management
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Manage IoT devices and their bindings with IoT cards (SIM cards), supporting device lifecycle management, device-card binding relationships, device-level package purchases, batch allocation, and remote device operations.
|
||||||
|
|
||||||
|
This capability supports:
|
||||||
|
- Device entity definition and lifecycle management
|
||||||
|
- Device-IoT card binding relationships (1-4 cards per device)
|
||||||
|
- Device-level package purchases with shared data pool
|
||||||
|
- Batch device allocation to agents
|
||||||
|
- Remote device operations (reboot, password change, reset)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 设备实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义设备(Device)实体,用于管理用户的物联网设备(如 GPS 追踪器、智能传感器等),支持设备与 IoT 卡的绑定关系、设备批量分配和设备操作。
|
||||||
|
|
||||||
|
**核心概念**: 设备不在卡管系统中销售,主要用于:
|
||||||
|
1. 用户设备管理(用户添加自己的设备,绑定 IoT 卡)
|
||||||
|
2. 方便运营人员管理投诉和代理要求(通过设备维度批量查看绑定的所有 IoT 卡)
|
||||||
|
3. 设备操作(重启、修改账号密码、重置等)
|
||||||
|
4. 设备批量分配(运营人员在别的系统报单后发货,把设备和绑定的 IoT 卡一起分配给代理)
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
|
||||||
|
**基本属性**:
|
||||||
|
- `id`: 设备 ID(主键,BIGINT)
|
||||||
|
- `device_no`: 设备编号(唯一,VARCHAR(50))
|
||||||
|
- `device_name`: 设备名称(VARCHAR(255))
|
||||||
|
- `device_model`: 设备型号(VARCHAR(100))
|
||||||
|
- `device_type`: 设备类型(VARCHAR(50),如 "GPS Tracker"、"Camera"、"Sensor")
|
||||||
|
- `max_sim_slots`: 最大 IoT 卡插槽数量(INT,1-4,默认 4)
|
||||||
|
- `manufacturer`: 设备制造商(VARCHAR(255),可选)
|
||||||
|
- `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯)
|
||||||
|
|
||||||
|
**所有权和状态**:
|
||||||
|
- `owner_type`: 所有者类型(VARCHAR(20),"platform"-平台库存(等待分配) | "agent"-代理商 | "user"-用户)
|
||||||
|
- `owner_id`: 所有者 ID(BIGINT,platform 时为 0,agent/user 时为对应的 ID)
|
||||||
|
- `status`: 设备状态(INT,1-未激活 2-已激活 3-已停用)
|
||||||
|
- `activated_at`: 激活时间(TIMESTAMP,可空)
|
||||||
|
|
||||||
|
**设备操作配置**(预留字段,用于后续设备操作功能):
|
||||||
|
- `device_username`: 设备登录账号(VARCHAR(100),可选)
|
||||||
|
- `device_password_encrypted`: 设备登录密码(加密存储,TEXT,可选)
|
||||||
|
- `device_api_endpoint`: 设备 API 接口地址(VARCHAR(500),可选)
|
||||||
|
|
||||||
|
**系统字段**:
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 用户添加设备
|
||||||
|
|
||||||
|
- **WHEN** 用户添加自己的设备(设备编号为 "GPS-001",设备名称为 "物流车辆追踪器")
|
||||||
|
- **THEN** 系统创建设备记录,`owner_type` 为 "user",`owner_id` 为用户 ID,状态为 1(未激活)
|
||||||
|
|
||||||
|
#### Scenario: 平台导入设备到库存
|
||||||
|
|
||||||
|
- **WHEN** 平台批量导入设备数据(准备发货给代理)
|
||||||
|
- **THEN** 系统创建设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
|
||||||
|
|
||||||
|
#### Scenario: 运营人员批量分配设备给代理
|
||||||
|
|
||||||
|
- **WHEN** 运营人员将平台库存设备(ID 为 1001)分配给代理商(用户 ID 为 123)
|
||||||
|
- **THEN** 系统将设备的 `owner_type` 变更为 "agent",`owner_id` 设置为 123,同时自动分配该设备绑定的所有 IoT 卡给代理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备状态流转
|
||||||
|
|
||||||
|
系统 SHALL 管理设备的状态流转,确保状态变更符合业务规则。
|
||||||
|
|
||||||
|
**状态定义**:
|
||||||
|
- **1-未激活**: 设备尚未激活使用
|
||||||
|
- **2-已激活**: 设备已被用户激活使用
|
||||||
|
- **3-已停用**: 设备已停用,不可使用
|
||||||
|
|
||||||
|
**状态流转规则**:
|
||||||
|
- 未激活(1) → 已激活(2): 用户激活设备
|
||||||
|
- 已激活(2) → 已停用(3): 用户或平台主动停用设备
|
||||||
|
- 已停用(3) → 已激活(2): 用户或平台主动恢复设备(仅在符合业务规则时)
|
||||||
|
|
||||||
|
#### Scenario: 用户激活设备
|
||||||
|
|
||||||
|
- **WHEN** 用户激活自己的设备
|
||||||
|
- **THEN** 系统将设备状态从 1(未激活) 变更为 2(已激活),`activated_at` 记录激活时间
|
||||||
|
|
||||||
|
#### Scenario: 用户停用设备
|
||||||
|
|
||||||
|
- **WHEN** 用户停用已激活的设备
|
||||||
|
- **THEN** 系统将设备状态从 2(已激活) 变更为 3(已停用),同时可选择是否停用该设备绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备与 IoT 卡绑定关系
|
||||||
|
|
||||||
|
系统 SHALL 管理设备与 IoT 卡的绑定关系,一个设备可以绑定 1-4 张 IoT 卡。
|
||||||
|
|
||||||
|
**绑定规则**:
|
||||||
|
- 一个设备最多绑定 4 张 IoT 卡(由 `max_sim_slots` 字段控制)
|
||||||
|
- 一个 IoT 卡同一时间只能绑定一个设备
|
||||||
|
- 绑定时记录插槽位置(slot_position: 1, 2, 3, 4)
|
||||||
|
- 绑定时记录绑定时间和绑定状态(1-已绑定 2-已解绑)
|
||||||
|
- 设备绑定 IoT 卡后,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为设备 ID
|
||||||
|
|
||||||
|
**中间表 device_sim_bindings**:
|
||||||
|
- `id`: 绑定记录 ID(主键,BIGINT)
|
||||||
|
- `device_id`: 设备 ID(BIGINT)
|
||||||
|
- `iot_card_id`: IoT 卡 ID(BIGINT)
|
||||||
|
- `slot_position`: 插槽位置(INT,1-4)
|
||||||
|
- `bind_status`: 绑定状态(INT,1-已绑定 2-已解绑)
|
||||||
|
- `bind_time`: 绑定时间(TIMESTAMP)
|
||||||
|
- `unbind_time`: 解绑时间(TIMESTAMP,可空)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 绑定 IoT 卡到设备
|
||||||
|
|
||||||
|
- **WHEN** 用户将 IoT 卡(ID 为 101)绑定到设备(ID 为 1001)的插槽 1
|
||||||
|
- **THEN** 系统创建绑定记录,`device_id` 为 1001,`iot_card_id` 为 101,`slot_position` 为 1,`bind_status` 为 1(已绑定),`bind_time` 为当前时间,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为 1001
|
||||||
|
|
||||||
|
#### Scenario: 绑定超过最大插槽数量
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试将第 5 张 IoT 卡绑定到最大插槽数为 4 的设备
|
||||||
|
- **THEN** 系统拒绝绑定,返回错误信息"设备插槽已满,最多支持 4 张 IoT 卡"
|
||||||
|
|
||||||
|
#### Scenario: 绑定已被占用的 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试绑定已被其他设备绑定的 IoT 卡
|
||||||
|
- **THEN** 系统拒绝绑定,返回错误信息"该 IoT 卡已被其他设备绑定"
|
||||||
|
|
||||||
|
#### Scenario: 解绑 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 用户解绑设备的 IoT 卡(绑定记录 ID 为 10)
|
||||||
|
- **THEN** 系统将绑定记录的 `bind_status` 从 1(已绑定) 变更为 2(已解绑),`unbind_time` 记录解绑时间,IoT 卡的 `owner_type` 和 `owner_id` 重置
|
||||||
|
|
||||||
|
#### Scenario: 查询设备当前绑定的 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 用户查询设备(ID 为 1001)当前绑定的 IoT 卡
|
||||||
|
- **THEN** 系统返回 `device_id` 为 1001 且 `bind_status` 为 1(已绑定) 的所有绑定记录,包含 IoT 卡信息(ICCID、运营商、激活状态等)和插槽位置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备套餐购买和流量共享
|
||||||
|
|
||||||
|
系统 SHALL 支持用户为设备购买套餐,套餐自动分配到设备绑定的所有 IoT 卡,流量在设备级别共享。
|
||||||
|
|
||||||
|
**设备套餐业务规则**:
|
||||||
|
- 用户为设备购买套餐时,套餐会分配到设备绑定的**所有 IoT 卡**(1-4 张)
|
||||||
|
- 套餐的流量是**设备级别共享的**(例如 3000G/月共享,不管用哪张卡)
|
||||||
|
- 分佣**只计算一次**(不按卡数倍增)
|
||||||
|
- 订单表通过 `device_id` 字段关联设备,通过 `device_sim_bindings` 表查找绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
**套餐分配示例**:
|
||||||
|
- 设备绑定 3 张 IoT 卡
|
||||||
|
- 用户购买套餐:399 元/年,每月 3000G 流量,长期佣金 100 元
|
||||||
|
- 用户支付:399 元
|
||||||
|
- 套餐分配:设备的 3 张 IoT 卡都获得该套餐
|
||||||
|
- 流量使用:3000G/月 在 3 张卡之间共享(不是每张卡 3000G,而是总共 3000G)
|
||||||
|
- 分佣:代理获得 100 元分佣(只分一次,不是 3 × 100 元)
|
||||||
|
|
||||||
|
#### Scenario: 用户为设备购买套餐
|
||||||
|
|
||||||
|
- **WHEN** 用户为设备(ID 为 1001,绑定 3 张 IoT 卡)购买套餐(套餐 ID 为 3001,399 元/年,3000G/月)
|
||||||
|
- **THEN** 系统创建套餐订单,`device_id` 为 1001,`package_id` 为 3001,订单金额为 399 元,将套餐分配到设备绑定的 3 张 IoT 卡,设置流量共享模式为设备级别
|
||||||
|
|
||||||
|
#### Scenario: 设备级流量共享
|
||||||
|
|
||||||
|
- **WHEN** 设备(ID 为 1001)的套餐流量为 3000G/月,设备绑定 3 张 IoT 卡
|
||||||
|
- **THEN** 系统设置流量共享模式,3 张 IoT 卡共享 3000G/月(不是每张卡 3000G),无论使用哪张卡,都从这个流量池扣除
|
||||||
|
|
||||||
|
#### Scenario: 设备套餐分佣
|
||||||
|
|
||||||
|
- **WHEN** 用户为设备购买套餐,订单金额为 399 元,代理的长期分佣规则为 100 元
|
||||||
|
- **THEN** 系统为代理创建一条分佣记录,分佣金额为 100 元(只分一次,不按设备绑定的卡数倍增)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备批量分配
|
||||||
|
|
||||||
|
系统 SHALL 支持运营人员批量分配设备给代理,设备分配时自动分配该设备绑定的所有 IoT 卡。
|
||||||
|
|
||||||
|
**分配规则**:
|
||||||
|
- 只能分配 `owner_type` 为 "platform" 的设备(平台库存)
|
||||||
|
- 分配时,设备的 `owner_type` 变更为 "agent",`owner_id` 设置为代理用户 ID
|
||||||
|
- 分配时,设备绑定的所有 IoT 卡的 `owner_type` 也变更为 "agent",`owner_id` 设置为代理用户 ID
|
||||||
|
- 分配操作记录到操作日志
|
||||||
|
|
||||||
|
#### Scenario: 运营人员批量分配设备
|
||||||
|
|
||||||
|
- **WHEN** 运营人员将 10 台设备(平台库存)分配给代理商(用户 ID 为 123)
|
||||||
|
- **THEN** 系统将这 10 台设备的 `owner_type` 变更为 "agent",`owner_id` 设置为 123,同时将这些设备绑定的所有 IoT 卡也分配给代理 123
|
||||||
|
|
||||||
|
#### Scenario: 分配已分配的设备
|
||||||
|
|
||||||
|
- **WHEN** 运营人员尝试分配 `owner_type` 为 "agent" 的设备
|
||||||
|
- **THEN** 系统拒绝分配,返回错误信息"该设备已分配给代理,不能重复分配"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备操作
|
||||||
|
|
||||||
|
系统 SHALL 支持对设备的远程操作(重启、修改账号密码、重置等),用于设备管理和故障排查。
|
||||||
|
|
||||||
|
**设备操作类型**:
|
||||||
|
- **重启设备**: 远程重启设备
|
||||||
|
- **修改账号密码**: 修改设备的登录账号和密码
|
||||||
|
- **重置设备**: 将设备恢复到出厂设置
|
||||||
|
- **查询设备状态**: 查询设备的在线状态、运行状态等
|
||||||
|
- **设备配置更新**: 更新设备的配置参数
|
||||||
|
|
||||||
|
**操作说明**:
|
||||||
|
- 本阶段只设计数据模型字段和接口定义,不实现设备操作的具体代码
|
||||||
|
- 后续 Service 层将调用设备厂商提供的 API 或通过 MQTT/HTTP 协议与设备通信
|
||||||
|
- 设备操作需要记录操作日志(操作类型、操作人、操作时间、操作结果)
|
||||||
|
|
||||||
|
#### Scenario: 重启设备
|
||||||
|
|
||||||
|
- **WHEN** 用户或运营人员请求重启设备(ID 为 1001)
|
||||||
|
- **THEN** 系统调用设备 API 发送重启命令,记录操作日志,返回操作结果
|
||||||
|
|
||||||
|
#### Scenario: 修改设备密码
|
||||||
|
|
||||||
|
- **WHEN** 用户或运营人员修改设备(ID 为 1001)的登录密码
|
||||||
|
- **THEN** 系统更新设备的 `device_password_encrypted` 字段(加密存储),调用设备 API 同步密码修改,记录操作日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备批量导入
|
||||||
|
|
||||||
|
系统 SHALL 支持批量导入设备数据,用于平台库存管理。
|
||||||
|
|
||||||
|
**导入字段**:
|
||||||
|
- 设备编号(必填)
|
||||||
|
- 设备名称(必填)
|
||||||
|
- 设备型号(必填)
|
||||||
|
- 设备类型(必填)
|
||||||
|
- 最大插槽数(可选,默认 4)
|
||||||
|
- 设备制造商(可选)
|
||||||
|
- 批次号(必填)
|
||||||
|
|
||||||
|
**导入规则**:
|
||||||
|
- 设备编号必须唯一,重复编号将被拒绝
|
||||||
|
- 导入的设备默认 `owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
|
||||||
|
- 导入成功后记录操作日志
|
||||||
|
|
||||||
|
#### Scenario: 批量导入设备成功
|
||||||
|
|
||||||
|
- **WHEN** 平台上传包含 50 条设备数据的 CSV 文件
|
||||||
|
- **THEN** 系统创建 50 条设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活),返回导入成功消息
|
||||||
|
|
||||||
|
#### Scenario: 批量导入包含重复编号
|
||||||
|
|
||||||
|
- **WHEN** 平台上传的 CSV 文件中包含已存在的设备编号
|
||||||
|
- **THEN** 系统拒绝重复编号的设备,返回错误信息并列出重复编号,其他有效设备正常导入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备查询和筛选
|
||||||
|
|
||||||
|
系统 SHALL 支持多维度查询和筛选设备,包括状态、所有者、批次号、设备类型等。
|
||||||
|
|
||||||
|
**查询条件**:
|
||||||
|
- 设备编号(精确匹配或模糊匹配)
|
||||||
|
- 设备名称(模糊匹配)
|
||||||
|
- 设备状态(单选或多选)
|
||||||
|
- 所有者类型(platform | agent | user)
|
||||||
|
- 所有者 ID(仅当所有者类型为 agent/user 时有效)
|
||||||
|
- 批次号(精确匹配)
|
||||||
|
- 设备类型(单选或多选)
|
||||||
|
- 设备制造商(模糊匹配)
|
||||||
|
- 激活时间范围(开始时间 - 结束时间)
|
||||||
|
- 创建时间范围(开始时间 - 结束时间)
|
||||||
|
|
||||||
|
**分页**:
|
||||||
|
- 默认每页 20 条,最大每页 100 条
|
||||||
|
- 返回总记录数和总页数
|
||||||
|
|
||||||
|
#### Scenario: 查询平台库存设备
|
||||||
|
|
||||||
|
- **WHEN** 运营人员查询平台库存设备
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "platform" 的设备列表
|
||||||
|
|
||||||
|
#### Scenario: 代理查询自己的设备
|
||||||
|
|
||||||
|
- **WHEN** 代理商(用户 ID 为 123)查询自己的设备
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 的设备列表
|
||||||
|
|
||||||
|
#### Scenario: 用户查询自己的设备
|
||||||
|
|
||||||
|
- **WHEN** 用户(用户 ID 为 2001)查询自己的设备
|
||||||
|
- **THEN** 系统返回 `owner_type` 为 "user" 且 `owner_id` 为 2001 的设备列表,包含设备绑定的所有 IoT 卡信息
|
||||||
|
|
||||||
|
#### Scenario: 运营人员通过设备查看绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
- **WHEN** 运营人员需要处理投诉,查询设备(ID 为 1001)绑定的所有 IoT 卡
|
||||||
|
- **THEN** 系统返回设备信息和绑定的所有 IoT 卡详细信息(ICCID、运营商、激活状态、流量使用等),方便统一查看和管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对设备数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 设备编号(device_no):必填,长度 1-50 字符,唯一
|
||||||
|
- 设备名称(device_name):必填,长度 1-255 字符
|
||||||
|
- 设备型号(device_model):必填,长度 1-100 字符
|
||||||
|
- 设备类型(device_type):必填,长度 1-50 字符
|
||||||
|
- 最大插槽数(max_sim_slots):必填,1-4 之间的整数
|
||||||
|
- 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user"
|
||||||
|
- 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
|
||||||
|
- 设备状态(status):必填,枚举值 1(未激活) | 2(已激活) | 3(已停用)
|
||||||
|
|
||||||
|
#### Scenario: 创建设备时插槽数超出范围
|
||||||
|
|
||||||
|
- **WHEN** 用户创建设备,最大插槽数为 5
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"最大插槽数必须在 1-4 之间"
|
||||||
|
|
||||||
|
#### Scenario: 创建设备时设备编号重复
|
||||||
|
|
||||||
|
- **WHEN** 用户创建设备,设备编号为已存在的 "DEV-001"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"设备编号已存在"
|
||||||
175
openspec/specs/iot-number-card/spec.md
Normal file
175
openspec/specs/iot-number-card/spec.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Number Card Management
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Manage number cards (virtual products) for carrier order callbacks, supporting carrier order passthrough, agent promotion, commission processing, and carrier settlement tracking.
|
||||||
|
|
||||||
|
This capability supports:
|
||||||
|
- Number card entity definition as virtual product mapping
|
||||||
|
- Carrier order callbacks from Gateway project
|
||||||
|
- Agent promotion via links or offline cards
|
||||||
|
- Commission processing for number card orders
|
||||||
|
- Carrier settlement tracking for financial reconciliation
|
||||||
|
- Integration with existing commission rules (one-time, long-term, combined)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 号卡实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义号卡(NumberCard)实体,作为运营商订单回传的映射,支持代理分销和分佣。
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
- `id`: 号卡 ID(主键,BIGINT)
|
||||||
|
- `virtual_product_code`: 虚拟商品编码(VARCHAR(100),唯一,用于对应运营商订单)
|
||||||
|
- `product_name`: 商品名称(VARCHAR(255))
|
||||||
|
- `carrier`: 运营商名称(VARCHAR(100),如 "中国移动"、"中国联通"、"中国电信")
|
||||||
|
- `carrier_product_id`: 运营商商品 ID(VARCHAR(100))
|
||||||
|
- `package_type`: 套餐类型(VARCHAR(50),如 "月套餐"、"流量包")
|
||||||
|
- `data_amount_mb`: 流量额度(BIGINT,MB 为单位,可选)
|
||||||
|
- `voice_minutes`: 语音分钟数(INT,可选)
|
||||||
|
- `sms_count`: 短信条数(INT,可选)
|
||||||
|
- `price`: 固定售价(DECIMAL(10,2),由运营商定价)
|
||||||
|
- `status`: 号卡状态(INT,1-上架 2-下架)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡商品
|
||||||
|
|
||||||
|
- **WHEN** 平台创建号卡商品,虚拟商品编码为 "VC-CMCC-001",运营商为"中国移动",固定售价为 30.00 元
|
||||||
|
- **THEN** 系统创建号卡记录,`virtual_product_code` 为 "VC-CMCC-001",`carrier` 为 "中国移动",`price` 为 30.00,状态为 1(上架)
|
||||||
|
|
||||||
|
#### Scenario: 虚拟商品编码唯一性
|
||||||
|
|
||||||
|
- **WHEN** 平台创建号卡商品,虚拟商品编码为已存在的 "VC-CMCC-001"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"虚拟商品编码已存在"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡运营商订单回传
|
||||||
|
|
||||||
|
系统 SHALL 接收 Gateway 项目转换后的运营商订单回传,通过虚拟商品编码匹配号卡,创建订单和分佣记录。
|
||||||
|
|
||||||
|
**订单回传字段**:
|
||||||
|
- `carrier_order_id`: 运营商订单 ID(VARCHAR(255),唯一)
|
||||||
|
- `virtual_product_code`: 虚拟商品编码(VARCHAR(100),用于匹配号卡)
|
||||||
|
- `user_phone`: 用户手机号(VARCHAR(20))
|
||||||
|
- `amount`: 订单金额(DECIMAL(10,2))
|
||||||
|
- `order_time`: 订单时间(TIMESTAMP)
|
||||||
|
- `agent_id`: 代理 ID(BIGINT,可空,如果通过代理推广则有值)
|
||||||
|
- `carrier_order_data`: 运营商订单原始数据(JSONB)
|
||||||
|
|
||||||
|
**回传处理流程**:
|
||||||
|
1. Gateway 接收运营商订单,统一转换为 JSON 格式
|
||||||
|
2. Gateway 通过 HTTP POST 回传给 CMP 系统
|
||||||
|
3. CMP 系统根据 `virtual_product_code` 匹配号卡
|
||||||
|
4. CMP 系统创建订单记录(`order_type` 为 "number_card")
|
||||||
|
5. 如果有 `agent_id`,触发代理分佣流程
|
||||||
|
|
||||||
|
#### Scenario: 接收运营商订单回传
|
||||||
|
|
||||||
|
- **WHEN** Gateway 回传运营商订单,虚拟商品编码为 "VC-CMCC-001",代理 ID 为 123,订单金额为 30.00 元
|
||||||
|
- **THEN** 系统创建订单记录,`order_type` 为 "number_card",`source_id` 为号卡 ID,`agent_id` 为 123,触发分佣计算
|
||||||
|
|
||||||
|
#### Scenario: 虚拟商品编码不存在
|
||||||
|
|
||||||
|
- **WHEN** Gateway 回传运营商订单,虚拟商品编码为不存在的 "VC-UNKNOWN"
|
||||||
|
- **THEN** 系统拒绝创建订单,返回错误信息"虚拟商品编码不存在"并记录到日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡代理分销
|
||||||
|
|
||||||
|
系统 SHALL 支持号卡的代理分销,代理通过推广链接或卡板推广号卡给终端用户。
|
||||||
|
|
||||||
|
**分销规则**:
|
||||||
|
- 号卡由运营商定价,平台无权修改价格
|
||||||
|
- 代理通过推广链接或卡板获取用户激活
|
||||||
|
- 用户激活充值后,资金直接支付给运营商,不经过平台
|
||||||
|
- 运营商周期性结算总佣金给平台
|
||||||
|
- 平台根据代理分佣规则分配佣金给代理
|
||||||
|
|
||||||
|
**代理推广方式**:
|
||||||
|
- **推广链接**: 代理生成带有 `agent_id` 的推广链接,用户点击链接激活
|
||||||
|
- **卡板**: 代理线下分发印有二维码的卡板,用户扫码激活
|
||||||
|
|
||||||
|
#### Scenario: 代理生成推广链接
|
||||||
|
|
||||||
|
- **WHEN** 代理商(用户 ID 为 123)为号卡(ID 为 5001)生成推广链接
|
||||||
|
- **THEN** 系统生成带有 `agent_id=123` 和 `product_id=5001` 的推广链接,如 `https://example.com/activate?agent=123&product=5001`
|
||||||
|
|
||||||
|
#### Scenario: 用户通过代理链接激活
|
||||||
|
|
||||||
|
- **WHEN** 用户通过代理推广链接激活号卡并充值 30.00 元
|
||||||
|
- **THEN** 运营商接收用户支付,Gateway 回传订单时包含 `agent_id=123`,系统触发代理分佣流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡分佣处理
|
||||||
|
|
||||||
|
系统 SHALL 根据号卡分佣规则计算代理佣金,支持冻结和解冻流程。
|
||||||
|
|
||||||
|
**分佣规则**:
|
||||||
|
- 号卡分佣配置在代理分佣规则表(`commission_rules`)中
|
||||||
|
- 分佣类型:一次性分佣、长期分佣、组合分佣(参考 iot-agent-commission 规范)
|
||||||
|
- 号卡订单的分佣需要满足条件:激活(实名) + 达到充值金额 + 在网状态 + 三无校验
|
||||||
|
- 分佣记录创建时状态为"冻结",满足条件后变为"解冻中",审批通过后变为"已发放"
|
||||||
|
|
||||||
|
#### Scenario: 号卡订单触发分佣
|
||||||
|
|
||||||
|
- **WHEN** 运营商回传订单,代理 ID 为 123,订单金额为 30.00 元,该代理配置了一次性分佣 5.00 元
|
||||||
|
- **THEN** 系统创建分佣记录,金额为 5.00 元,状态为"冻结",等待满足解冻条件
|
||||||
|
|
||||||
|
#### Scenario: 号卡分佣解冻
|
||||||
|
|
||||||
|
- **WHEN** 号卡订单满足解冻条件(激活 + 充值 + 在网 + 三无校验)
|
||||||
|
- **THEN** 系统将分佣记录状态从"冻结"变更为"解冻中",创建分佣解冻审批记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡运营商结算
|
||||||
|
|
||||||
|
系统 SHALL 记录运营商周期性结算的佣金总额,用于财务对账和利润计算。
|
||||||
|
|
||||||
|
**结算字段**:
|
||||||
|
- `settlement_id`: 结算记录 ID(主键,BIGINT)
|
||||||
|
- `carrier`: 运营商名称(VARCHAR(100))
|
||||||
|
- `settlement_period`: 结算周期(VARCHAR(50),如 "2025-01")
|
||||||
|
- `total_commission`: 运营商结算的佣金总额(DECIMAL(18,2))
|
||||||
|
- `settlement_time`: 结算时间(TIMESTAMP)
|
||||||
|
- `status`: 结算状态(INT,1-待确认 2-已确认)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 记录运营商结算
|
||||||
|
|
||||||
|
- **WHEN** 运营商"中国移动"结算 2025 年 1 月的佣金总额 50000.00 元
|
||||||
|
- **THEN** 系统创建结算记录,`carrier` 为 "中国移动",`settlement_period` 为 "2025-01",`total_commission` 为 50000.00,状态为 1(待确认)
|
||||||
|
|
||||||
|
#### Scenario: 确认运营商结算
|
||||||
|
|
||||||
|
- **WHEN** 财务确认运营商结算记录(ID 为 1001)
|
||||||
|
- **THEN** 系统将结算记录状态从 1(待确认) 变更为 2(已确认)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 号卡数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对号卡数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 虚拟商品编码(virtual_product_code):必填,长度 1-100 字符,唯一
|
||||||
|
- 商品名称(product_name):必填,长度 1-255 字符
|
||||||
|
- 运营商名称(carrier):必填,长度 1-100 字符
|
||||||
|
- 固定售价(price):必填,≥ 0,最多 2 位小数
|
||||||
|
- 状态(status):必填,枚举值 1(上架) | 2(下架)
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡时虚拟商品编码为空
|
||||||
|
|
||||||
|
- **WHEN** 平台创建号卡,虚拟商品编码为空
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"虚拟商品编码不能为空"
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡时固定售价为负数
|
||||||
|
|
||||||
|
- **WHEN** 平台创建号卡,固定售价为 -10.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"固定售价必须 ≥ 0"
|
||||||
248
openspec/specs/iot-order/spec.md
Normal file
248
openspec/specs/iot-order/spec.md
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
# IoT Order Management
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Manage orders for IoT card packages and number card products, including order creation, payment processing, status tracking, commission triggering, and support for single-card orders, device-level orders, and carrier number card orders.
|
||||||
|
|
||||||
|
This capability supports:
|
||||||
|
- Unified order entity for package orders and number card orders
|
||||||
|
- Order status lifecycle management
|
||||||
|
- Multiple payment methods (wallet, online payment, carrier direct payment)
|
||||||
|
- Commission triggering on order completion
|
||||||
|
- Device-level order commission (counted once regardless of bound card count)
|
||||||
|
- Multi-dimensional order querying and filtering
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 订单实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义订单(Order)实体,统一管理两种订单类型:套餐订单、号卡订单。
|
||||||
|
|
||||||
|
**核心概念**:
|
||||||
|
- **套餐订单**: 用户为 IoT 卡或设备购买套餐的订单,包括单卡套餐订单和设备级套餐订单
|
||||||
|
- **号卡订单**: 运营商回传的号卡订单,用户直接在上游平台下单,系统只接收订单状态更新
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
- `id`: 订单 ID(主键,BIGINT)
|
||||||
|
- `order_no`: 订单编号(VARCHAR(50),唯一)
|
||||||
|
- `order_type`: 订单类型(INT,1-套餐订单 2-号卡订单)
|
||||||
|
- `iot_card_id`: IoT 卡 ID(BIGINT,可空,单卡套餐订单时有值)
|
||||||
|
- `device_id`: 设备 ID(BIGINT,可空,设备级套餐订单时有值)
|
||||||
|
- `number_card_id`: 号卡 ID(BIGINT,可空,号卡订单时有值)
|
||||||
|
- `package_id`: 套餐 ID(BIGINT,可空,仅当 order_type 为 1 时有值)
|
||||||
|
- `user_id`: 用户 ID(BIGINT,购买用户)
|
||||||
|
- `agent_id`: 代理 ID(BIGINT,可空,通过代理购买时有值)
|
||||||
|
- `amount`: 订单金额(DECIMAL(10,2),元)
|
||||||
|
- `payment_method`: 支付方式(VARCHAR(20),"wallet"-钱包 | "online"-在线支付 | "carrier"-运营商直付)
|
||||||
|
- `status`: 订单状态(INT,1-待支付 2-已支付 3-已完成 4-已取消 5-已退款)
|
||||||
|
- `carrier_order_id`: 运营商订单 ID(VARCHAR(255),可空,仅号卡订单有值)
|
||||||
|
- `carrier_order_data`: 运营商订单原始数据(JSONB,可空)
|
||||||
|
- `paid_at`: 支付时间(TIMESTAMP,可空)
|
||||||
|
- `completed_at`: 完成时间(TIMESTAMP,可空)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
**订单类型说明**:
|
||||||
|
- **单卡套餐订单**: `order_type` 为 1,`iot_card_id` 有值,`device_id` 为 NULL
|
||||||
|
- **设备级套餐订单**: `order_type` 为 1,`device_id` 有值,`iot_card_id` 为 NULL
|
||||||
|
- **号卡订单**: `order_type` 为 2,`number_card_id` 有值,`iot_card_id` 和 `device_id` 为 NULL
|
||||||
|
|
||||||
|
#### Scenario: 创建单卡套餐购买订单
|
||||||
|
|
||||||
|
- **WHEN** 用户(ID 为 2001)为 IoT 卡(ID 为 1001)购买套餐(ID 为 3001),金额为 30.00 元
|
||||||
|
- **THEN** 系统创建订单记录,`order_type` 为 1,`iot_card_id` 为 1001,`device_id` 为 NULL,`package_id` 为 3001,`user_id` 为 2001,`amount` 为 30.00,状态为 1(待支付)
|
||||||
|
|
||||||
|
#### Scenario: 创建设备级套餐购买订单
|
||||||
|
|
||||||
|
- **WHEN** 用户(ID 为 2001)为设备(ID 为 5001,绑定 3 张 IoT 卡)购买套餐(ID 为 3002),金额为 399.00 元
|
||||||
|
- **THEN** 系统创建订单记录,`order_type` 为 1,`device_id` 为 5001,`iot_card_id` 为 NULL,`package_id` 为 3002,`user_id` 为 2001,`amount` 为 399.00,状态为 1(待支付)
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡订单(运营商回传)
|
||||||
|
|
||||||
|
- **WHEN** Gateway 回传运营商订单,虚拟商品编码对应号卡 ID 为 6001,代理 ID 为 123,订单金额为 30.00 元
|
||||||
|
- **THEN** 系统创建订单记录,`order_type` 为 2,`number_card_id` 为 6001,`iot_card_id` 为 NULL,`device_id` 为 NULL,`agent_id` 为 123,`amount` 为 30.00,`payment_method` 为 "carrier",状态为 2(已支付)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单状态流转
|
||||||
|
|
||||||
|
系统 SHALL 管理订单的状态流转,确保状态变更符合业务规则。
|
||||||
|
|
||||||
|
**状态定义**:
|
||||||
|
- **1-待支付**: 订单已创建,等待用户支付
|
||||||
|
- **2-已支付**: 用户已支付,等待系统处理
|
||||||
|
- **3-已完成**: 订单已完成(激活/发货等)
|
||||||
|
- **4-已取消**: 订单已取消
|
||||||
|
- **5-已退款**: 订单已退款
|
||||||
|
|
||||||
|
**状态流转规则**:
|
||||||
|
- 待支付(1) → 已支付(2): 用户完成支付
|
||||||
|
- 待支付(1) → 已取消(4): 用户取消订单或订单超时
|
||||||
|
- 已支付(2) → 已完成(3): 系统完成订单处理(激活/发货)
|
||||||
|
- 已支付(2) → 已退款(5): 用户申请退款且审核通过
|
||||||
|
- 已完成(3) → 已退款(5): 用户申请退款且审核通过(特殊情况)
|
||||||
|
|
||||||
|
#### Scenario: 用户支付订单
|
||||||
|
|
||||||
|
- **WHEN** 用户支付待支付订单(ID 为 10001),支付金额为 30.00 元
|
||||||
|
- **THEN** 系统将订单状态从 1(待支付) 变更为 2(已支付),`paid_at` 记录支付时间
|
||||||
|
|
||||||
|
#### Scenario: 单卡套餐订单完成
|
||||||
|
|
||||||
|
- **WHEN** 系统处理完单卡套餐订单(ID 为 10001),激活 IoT 卡并分配套餐
|
||||||
|
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
|
||||||
|
|
||||||
|
#### Scenario: 设备级套餐订单完成
|
||||||
|
|
||||||
|
- **WHEN** 系统处理完设备级套餐订单(ID 为 10002),为设备绑定的所有 IoT 卡分配套餐
|
||||||
|
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单支付方式
|
||||||
|
|
||||||
|
系统 SHALL 支持三种支付方式:钱包支付、在线支付、运营商直付。
|
||||||
|
|
||||||
|
**支付方式**:
|
||||||
|
- **钱包支付(wallet)**: 从用户钱包余额扣款
|
||||||
|
- **在线支付(online)**: 通过第三方支付(微信/支付宝等)
|
||||||
|
- **运营商直付(carrier)**: 用户直接支付给运营商(仅号卡订单)
|
||||||
|
|
||||||
|
**支付规则**:
|
||||||
|
- 一次性分佣订单必须使用钱包支付
|
||||||
|
- 套餐购买订单可以使用钱包或在线支付
|
||||||
|
- 号卡订单必须使用运营商直付
|
||||||
|
|
||||||
|
#### Scenario: 钱包支付订单
|
||||||
|
|
||||||
|
- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 50.00 元
|
||||||
|
- **THEN** 系统从钱包扣除 30.00 元,订单状态变更为 2(已支付),`payment_method` 为 "wallet"
|
||||||
|
|
||||||
|
#### Scenario: 钱包余额不足
|
||||||
|
|
||||||
|
- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 20.00 元
|
||||||
|
- **THEN** 系统拒绝支付,返回错误信息"钱包余额不足"
|
||||||
|
|
||||||
|
#### Scenario: 一次性分佣订单强制钱包支付
|
||||||
|
|
||||||
|
- **WHEN** 用户购买配置了一次性分佣的套餐,尝试使用在线支付
|
||||||
|
- **THEN** 系统拒绝支付,返回错误信息"一次性分佣订单必须使用钱包支付"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单分佣触发
|
||||||
|
|
||||||
|
系统 SHALL 在订单完成时触发分佣计算,根据代理分佣规则创建分佣记录。
|
||||||
|
|
||||||
|
**触发条件**:
|
||||||
|
- 订单状态变更为 3(已完成)
|
||||||
|
- 订单有 `agent_id`(通过代理销售)
|
||||||
|
- 代理配置了分佣规则
|
||||||
|
|
||||||
|
**分佣计算规则**:
|
||||||
|
- **单卡套餐订单**: 根据 IoT 卡关联的代理分佣规则计算分佣
|
||||||
|
- **设备级套餐订单**: 分佣只计算一次(不按设备绑定的 IoT 卡数量倍增)
|
||||||
|
- **号卡订单**: 下单即冻结分佣,次月通过 Excel 导入解冻
|
||||||
|
|
||||||
|
#### Scenario: 单卡套餐购买订单触发分佣
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)的单卡套餐订单(ID 为 10001)完成,订单金额为 30.00 元,代理配置了 5.00 元一次性分佣
|
||||||
|
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结)
|
||||||
|
|
||||||
|
#### Scenario: 设备级套餐订单触发分佣(只计算一次)
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)的设备级套餐订单(ID 为 10002)完成,设备绑定 3 张 IoT 卡,订单金额为 399.00 元,代理配置了 100.00 元长期分佣
|
||||||
|
- **THEN** 系统创建一条分佣记录,`agent_id` 为 123,`order_id` 为 10002,`amount` 为 100.00,状态为 1(冻结),不是 3 × 100.00
|
||||||
|
|
||||||
|
#### Scenario: 号卡订单触发分佣
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)的号卡订单(ID 为 10003)创建,订单金额为 30.00 元,代理配置了长期分佣
|
||||||
|
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10003,状态为 1(冻结),等待次月通过 Excel 导入解冻
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单查询和筛选
|
||||||
|
|
||||||
|
系统 SHALL 支持多维度查询和筛选订单。
|
||||||
|
|
||||||
|
**查询条件**:
|
||||||
|
- 订单编号(精确匹配)
|
||||||
|
- 订单类型(1-套餐订单 2-号卡订单)
|
||||||
|
- 订单状态(单选或多选)
|
||||||
|
- IoT 卡 ID(精确匹配)
|
||||||
|
- 设备 ID(精确匹配)
|
||||||
|
- 号卡 ID(精确匹配)
|
||||||
|
- 用户 ID(精确匹配)
|
||||||
|
- 代理 ID(精确匹配)
|
||||||
|
- 支付方式(单选或多选)
|
||||||
|
- 创建时间范围(开始时间 - 结束时间)
|
||||||
|
- 支付时间范围(开始时间 - 结束时间)
|
||||||
|
- 完成时间范围(开始时间 - 结束时间)
|
||||||
|
|
||||||
|
**分页**:
|
||||||
|
- 默认每页 20 条,最大每页 100 条
|
||||||
|
- 返回总记录数和总页数
|
||||||
|
|
||||||
|
#### Scenario: 查询用户的所有订单
|
||||||
|
|
||||||
|
- **WHEN** 用户(ID 为 2001)查询自己的所有订单
|
||||||
|
- **THEN** 系统返回 `user_id` 为 2001 的所有订单列表,按创建时间倒序排列
|
||||||
|
|
||||||
|
#### Scenario: 查询代理的订单
|
||||||
|
|
||||||
|
- **WHEN** 代理(ID 为 123)查询自己的订单,筛选已完成的套餐订单
|
||||||
|
- **THEN** 系统返回 `agent_id` 为 123 且 `order_type` 为 1 且 `status` 为 3(已完成) 的订单列表
|
||||||
|
|
||||||
|
#### Scenario: 查询 IoT 卡的订单历史
|
||||||
|
|
||||||
|
- **WHEN** 运营人员查询 IoT 卡(ID 为 1001)的所有订单
|
||||||
|
- **THEN** 系统返回 `iot_card_id` 为 1001 的所有订单列表,包含套餐购买记录
|
||||||
|
|
||||||
|
#### Scenario: 查询设备的订单历史
|
||||||
|
|
||||||
|
- **WHEN** 运营人员查询设备(ID 为 5001)的所有订单
|
||||||
|
- **THEN** 系统返回 `device_id` 为 5001 的所有设备级套餐订单列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 订单数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对订单数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 订单编号(order_no):必填,长度 1-50 字符,唯一
|
||||||
|
- 订单类型(order_type):必填,枚举值 1(套餐订单) | 2(号卡订单)
|
||||||
|
- IoT 卡 ID(iot_card_id):套餐订单时 iot_card_id 和 device_id 二选一
|
||||||
|
- 设备 ID(device_id):套餐订单时 iot_card_id 和 device_id 二选一
|
||||||
|
- 号卡 ID(number_card_id):号卡订单时必填
|
||||||
|
- 套餐 ID(package_id):套餐订单时必填
|
||||||
|
- 用户 ID(user_id):必填,≥ 1
|
||||||
|
- 订单金额(amount):必填,≥ 0,最多 2 位小数
|
||||||
|
- 支付方式(payment_method):必填,枚举值 "wallet" | "online" | "carrier"
|
||||||
|
- 状态(status):必填,枚举值 1-5
|
||||||
|
|
||||||
|
#### Scenario: 创建订单时金额为负数
|
||||||
|
|
||||||
|
- **WHEN** 创建订单,金额为 -10.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"订单金额必须 ≥ 0"
|
||||||
|
|
||||||
|
#### Scenario: 创建订单时订单编号重复
|
||||||
|
|
||||||
|
- **WHEN** 创建订单,订单编号为已存在的 "ORD-2025-001"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"订单编号已存在"
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐订单时未关联 IoT 卡或设备
|
||||||
|
|
||||||
|
- **WHEN** 创建套餐订单,`iot_card_id` 和 `device_id` 都为 NULL
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"套餐订单必须关联 IoT 卡或设备"
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐订单时同时关联 IoT 卡和设备
|
||||||
|
|
||||||
|
- **WHEN** 创建套餐订单,`iot_card_id` 为 1001,`device_id` 为 5001
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"套餐订单不能同时关联 IoT 卡和设备"
|
||||||
|
|
||||||
|
#### Scenario: 创建号卡订单时未关联号卡
|
||||||
|
|
||||||
|
- **WHEN** 创建号卡订单,`number_card_id` 为 NULL
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"号卡订单必须关联号卡"
|
||||||
226
openspec/specs/iot-package/spec.md
Normal file
226
openspec/specs/iot-package/spec.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# IoT Package Management
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Manage IoT packages (data plans) for IoT cards and devices, including package definitions, real/virtual data coexistence, single-card packages, device-level packages with shared data pools, and agent package allocation.
|
||||||
|
|
||||||
|
This capability supports:
|
||||||
|
- Package entity definition with real and virtual data types
|
||||||
|
- Formal packages and addon packages (data top-ups)
|
||||||
|
- Single-card package purchases
|
||||||
|
- Device-level package purchases with shared data pool across all bound cards
|
||||||
|
- Agent package allocation with retail pricing
|
||||||
|
- Commission calculation (counted once for device-level packages regardless of card count)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 套餐实体定义
|
||||||
|
|
||||||
|
系统 SHALL 定义套餐(Package)实体,包含套餐的基本属性、定价、流量配置。
|
||||||
|
|
||||||
|
**核心概念**: 套餐只适用于 IoT 卡(ICCID),用户可以为单张 IoT 卡购买套餐,也可以为设备购买套餐(套餐分配到设备绑定的所有 IoT 卡,流量设备级共享)。
|
||||||
|
|
||||||
|
**实体字段**:
|
||||||
|
- `id`: 套餐 ID(主键,BIGINT)
|
||||||
|
- `package_code`: 套餐编码(VARCHAR(50),唯一)
|
||||||
|
- `package_name`: 套餐名称(VARCHAR(255))
|
||||||
|
- `series_id`: 套餐系列 ID(BIGINT,关联 package_series 表,用于组织套餐分组和配置一次性分佣)
|
||||||
|
- `package_type`: 套餐类型(VARCHAR(20),"formal"-正式套餐 | "addon"-加油包)
|
||||||
|
- `duration_months`: 套餐时长(INT,月数,1-月套餐 12-年套餐,加油包为 0)
|
||||||
|
- `real_data_mb`: 真流量额度(BIGINT,MB 为单位,可选)
|
||||||
|
- `virtual_data_mb`: 虚流量额度(BIGINT,MB 为单位,用于停机判断,可选)
|
||||||
|
- `data_amount_mb`: 总流量额度(BIGINT,MB 为单位,real_data_mb + virtual_data_mb)
|
||||||
|
- `price`: 套餐价格(DECIMAL(10,2),元)
|
||||||
|
- `status`: 套餐状态(INT,1-上架 2-下架)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
**套餐类型说明**:
|
||||||
|
- **正式套餐(formal)**: 每张 IoT 卡只能有一个有效的正式套餐,购买新的正式套餐会替换旧的
|
||||||
|
- **加油包(addon)**: 每张 IoT 卡可以购买多个加油包,与正式套餐共存
|
||||||
|
|
||||||
|
#### Scenario: 创建月套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建月套餐,套餐编码为 "PKG-M-001",套餐名称为 "月套餐 10GB",套餐系列 ID 为 1,类型为正式套餐,时长为 1 个月,真流量为 10240 MB,虚流量为 0,价格为 30.00 元
|
||||||
|
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-M-001",`series_id` 为 1,`package_type` 为 "formal",`duration_months` 为 1,`real_data_mb` 为 10240,`virtual_data_mb` 为 0,`data_amount_mb` 为 10240,`price` 为 30.00
|
||||||
|
|
||||||
|
#### Scenario: 创建年套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建年套餐,套餐编码为 "PKG-Y-001",套餐名称为 "年套餐 120GB",套餐系列 ID 为 1,类型为正式套餐,时长为 12 个月,真流量为 122880 MB,虚流量为 0,价格为 300.00 元
|
||||||
|
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-Y-001",`series_id` 为 1,`package_type` 为 "formal",`duration_months` 为 12,`real_data_mb` 为 122880,`virtual_data_mb` 为 0,`data_amount_mb` 为 122880,`price` 为 300.00
|
||||||
|
|
||||||
|
#### Scenario: 创建流量加油包
|
||||||
|
|
||||||
|
- **WHEN** 平台创建加油包,套餐编码为 "PKG-ADD-001",套餐名称为 "流量包 5GB",套餐系列 ID 为 2,类型为加油包,时长为 0,真流量为 5120 MB,虚流量为 0,价格为 10.00 元
|
||||||
|
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-ADD-001",`series_id` 为 2,`package_type` 为 "addon",`duration_months` 为 0,`real_data_mb` 为 5120,`virtual_data_mb` 为 0,`data_amount_mb` 为 5120,`price` 为 10.00
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 套餐流量类型和真虚流量共存
|
||||||
|
|
||||||
|
系统 SHALL 支持真流量和虚流量两种流量类型,两者可以共存于同一套餐中。
|
||||||
|
|
||||||
|
**流量类型定义**:
|
||||||
|
- **真流量(real_data_mb)**: 实际可用的流量,可在运营商网络中使用
|
||||||
|
- **虚流量(virtual_data_mb)**: 虚拟流量,用于停机判断(虚流量用完后停机,即使真流量还有剩余)
|
||||||
|
- **总流量(data_amount_mb)**: 真流量 + 虚流量的总和
|
||||||
|
|
||||||
|
**重要规则**:
|
||||||
|
- 真流量和虚流量可以同时存在于一个套餐中
|
||||||
|
- 停机判断基于虚流量(虚流量用完后停机)
|
||||||
|
- 套餐可以只有真流量、只有虚流量、或两者都有
|
||||||
|
|
||||||
|
#### Scenario: 创建真虚流量共存的套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,真流量为 8000 MB,虚流量为 2000 MB
|
||||||
|
- **THEN** 系统创建套餐记录,`real_data_mb` 为 8000,`virtual_data_mb` 为 2000,`data_amount_mb` 为 10000
|
||||||
|
|
||||||
|
#### Scenario: 创建纯真流量套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,真流量为 10240 MB,虚流量为 0
|
||||||
|
- **THEN** 系统创建套餐记录,`real_data_mb` 为 10240,`virtual_data_mb` 为 0,`data_amount_mb` 为 10240
|
||||||
|
|
||||||
|
#### Scenario: 创建纯虚流量套餐
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,真流量为 0,虚流量为 10240 MB
|
||||||
|
- **THEN** 系统创建套餐记录,`real_data_mb` 为 0,`virtual_data_mb` 为 10240,`data_amount_mb` 为 10240
|
||||||
|
|
||||||
|
#### Scenario: 虚流量用完停机
|
||||||
|
|
||||||
|
- **WHEN** 套餐的虚流量为 2000 MB,用户已使用 2000 MB 虚流量,但真流量还剩余 5000 MB
|
||||||
|
- **THEN** 系统判断虚流量已用完,触发停机操作,即使真流量还有剩余
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 单卡套餐购买
|
||||||
|
|
||||||
|
系统 SHALL 支持用户为单张 IoT 卡购买套餐。
|
||||||
|
|
||||||
|
**购买规则**:
|
||||||
|
- 每张 IoT 卡只能有一个有效的正式套餐
|
||||||
|
- 购买新的正式套餐会替换旧的正式套餐
|
||||||
|
- 可以同时购买多个加油包
|
||||||
|
- 套餐购买后创建套餐订单记录
|
||||||
|
|
||||||
|
#### Scenario: 为 IoT 卡购买正式套餐
|
||||||
|
|
||||||
|
- **WHEN** 用户为 IoT 卡(ICCID 为 "8986...")购买月套餐(套餐 ID 为 1001),价格为 30.00 元
|
||||||
|
- **THEN** 系统创建套餐订单,`order_type` 为 1(套餐订单),`iot_card_id` 为 IoT 卡 ID,`package_id` 为 1001,`amount` 为 30.00
|
||||||
|
|
||||||
|
#### Scenario: 为 IoT 卡购买加油包
|
||||||
|
|
||||||
|
- **WHEN** 用户为 IoT 卡购买流量加油包(套餐 ID 为 2001),价格为 10.00 元
|
||||||
|
- **THEN** 系统创建套餐订单,IoT 卡的正式套餐保持不变,加油包作为额外套餐生效
|
||||||
|
|
||||||
|
#### Scenario: 购买新正式套餐替换旧套餐
|
||||||
|
|
||||||
|
- **WHEN** 用户为 IoT 卡购买新的月套餐,该 IoT 卡已有月套餐
|
||||||
|
- **THEN** 系统创建新订单,旧的正式套餐失效,新套餐生效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 设备级套餐购买和流量共享
|
||||||
|
|
||||||
|
系统 SHALL 支持用户为设备购买套餐,套餐分配到设备绑定的所有 IoT 卡,流量设备级共享。
|
||||||
|
|
||||||
|
**设备套餐业务规则**:
|
||||||
|
- 用户为设备购买套餐时,套餐会分配到设备绑定的**所有 IoT 卡**(1-4 张)
|
||||||
|
- 套餐的流量是**设备级别共享的**(例如 3000G/月共享,不管用哪张卡)
|
||||||
|
- 分佣**只计算一次**(不按卡数倍增)
|
||||||
|
- 订单表通过 `device_id` 字段关联设备,通过 `device_sim_bindings` 表查找绑定的所有 IoT 卡
|
||||||
|
- 设备购买的套餐不受单卡套餐限制(设备套餐和单卡套餐独立管理)
|
||||||
|
|
||||||
|
**流量共享机制**:
|
||||||
|
- 设备绑定的所有 IoT 卡共享套餐流量池
|
||||||
|
- 任意一张 IoT 卡使用流量都会从共享池扣除
|
||||||
|
- 流量池耗尽后,所有绑定的 IoT 卡都无法使用
|
||||||
|
|
||||||
|
**订单记录**:
|
||||||
|
- 订单表 `device_id` 字段记录设备 ID(设备级套餐订单)
|
||||||
|
- 订单表 `iot_card_id` 字段为 NULL(不关联具体 IoT 卡)
|
||||||
|
- 通过 `device_sim_bindings` 表查询设备绑定的所有 IoT 卡
|
||||||
|
|
||||||
|
#### Scenario: 为设备购买套餐
|
||||||
|
|
||||||
|
- **WHEN** 用户为设备(ID 为 1001,绑定 3 张 IoT 卡)购买年套餐,价格为 399.00 元,流量为 3000G/月
|
||||||
|
- **THEN** 系统创建套餐订单,`order_type` 为 1(套餐订单),`device_id` 为 1001,`iot_card_id` 为 NULL,`amount` 为 399.00,套餐分配到 3 张绑定的 IoT 卡
|
||||||
|
|
||||||
|
#### Scenario: 设备流量共享
|
||||||
|
|
||||||
|
- **WHEN** 设备(绑定 3 张 IoT 卡)购买套餐 3000G/月,其中一张 IoT 卡使用 1000G 流量
|
||||||
|
- **THEN** 流量池剩余 2000G,其他两张 IoT 卡可以使用剩余的 2000G
|
||||||
|
|
||||||
|
#### Scenario: 设备套餐分佣只计算一次
|
||||||
|
|
||||||
|
- **WHEN** 设备(绑定 3 张 IoT 卡)购买套餐,长期佣金为 100.00 元
|
||||||
|
- **THEN** 系统创建一条分佣记录,金额为 100.00 元(不是 3 × 100.00 元)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 套餐分配给代理
|
||||||
|
|
||||||
|
系统 SHALL 支持将套餐分配给代理商,代理可以在平台设置的成本价基础上加价销售。
|
||||||
|
|
||||||
|
**分配规则**:
|
||||||
|
- 平台为套餐设置成本价(分配给代理的价格)
|
||||||
|
- 代理可以在成本价基础上加价,但不能超过成本价的 2 倍
|
||||||
|
- 分配记录存储在 `agent_package_allocations` 表
|
||||||
|
|
||||||
|
**agent_package_allocations 表**:
|
||||||
|
- `id`: 分配记录 ID(主键,BIGINT)
|
||||||
|
- `agent_id`: 代理用户 ID(BIGINT)
|
||||||
|
- `package_id`: 套餐 ID(BIGINT)
|
||||||
|
- `cost_price`: 成本价(DECIMAL(10,2),平台给代理的价格)
|
||||||
|
- `retail_price`: 零售价(DECIMAL(10,2),代理设置的终端销售价格)
|
||||||
|
- `status`: 分配状态(INT,1-有效 2-无效)
|
||||||
|
- `created_at`: 创建时间(TIMESTAMP,自动填充)
|
||||||
|
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
|
||||||
|
|
||||||
|
#### Scenario: 平台分配套餐给代理
|
||||||
|
|
||||||
|
- **WHEN** 平台将套餐(ID 为 1001)分配给代理(用户 ID 为 123),成本价为 25.00 元
|
||||||
|
- **THEN** 系统创建分配记录,`agent_id` 为 123,`package_id` 为 1001,`cost_price` 为 25.00,状态为 1(有效)
|
||||||
|
|
||||||
|
#### Scenario: 代理设置零售价
|
||||||
|
|
||||||
|
- **WHEN** 代理(用户 ID 为 123)为套餐(ID 为 1001)设置零售价为 30.00 元
|
||||||
|
- **THEN** 系统更新分配记录,`retail_price` 为 30.00
|
||||||
|
|
||||||
|
#### Scenario: 代理零售价超过 2 倍成本价
|
||||||
|
|
||||||
|
- **WHEN** 代理设置零售价为 60.00 元,成本价为 25.00 元(2 倍为 50.00 元)
|
||||||
|
- **THEN** 系统拒绝设置,返回错误信息"零售价不能超过成本价的 2 倍"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 套餐数据校验
|
||||||
|
|
||||||
|
系统 SHALL 对套餐数据进行校验,确保数据完整性和一致性。
|
||||||
|
|
||||||
|
**校验规则**:
|
||||||
|
- 套餐编码(package_code):必填,长度 1-50 字符,唯一
|
||||||
|
- 套餐名称(package_name):必填,长度 1-255 字符
|
||||||
|
- 套餐系列 ID(series_id):必填,≥ 1,必须是有效的套餐系列 ID
|
||||||
|
- 套餐类型(package_type):必填,枚举值 "formal" | "addon"
|
||||||
|
- 套餐时长(duration_months):必填,≥ 0(正式套餐 ≥ 1,加油包为 0)
|
||||||
|
- 真流量额度(real_data_mb):可选,≥ 0
|
||||||
|
- 虚流量额度(virtual_data_mb):可选,≥ 0
|
||||||
|
- 总流量额度(data_amount_mb):必填,≥ 0,必须等于 real_data_mb + virtual_data_mb
|
||||||
|
- 套餐价格(price):必填,≥ 0,最多 2 位小数
|
||||||
|
- 状态(status):必填,枚举值 1(上架) | 2(下架)
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐时价格为负数
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,价格为 -10.00
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"套餐价格必须 ≥ 0"
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐时套餐编码重复
|
||||||
|
|
||||||
|
- **WHEN** 平台创建套餐,套餐编码为已存在的 "PKG-M-001"
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"套餐编码已存在"
|
||||||
|
|
||||||
|
#### Scenario: 创建正式套餐时时长为 0
|
||||||
|
|
||||||
|
- **WHEN** 平台创建正式套餐,套餐类型为 "formal",时长为 0
|
||||||
|
- **THEN** 系统拒绝创建,返回错误信息"正式套餐时长必须 ≥ 1"
|
||||||
240
pkg/constants/iot.go
Normal file
240
pkg/constants/iot.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// IoT SIM 管理系统常量定义
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 1. 核心业务常量
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// IoT 卡状态
|
||||||
|
const (
|
||||||
|
IotCardStatusInStock = 1 // 在库
|
||||||
|
IotCardStatusDistributed = 2 // 已分销
|
||||||
|
IotCardStatusActivated = 3 // 已激活
|
||||||
|
IotCardStatusSuspended = 4 // 已停用
|
||||||
|
)
|
||||||
|
|
||||||
|
// IoT 卡业务类型
|
||||||
|
const (
|
||||||
|
CardCategoryNormal = "normal" // 普通卡(需要实名认证)
|
||||||
|
CardCategoryIndustry = "industry" // 行业卡(无需实名认证)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 设备状态
|
||||||
|
const (
|
||||||
|
DeviceStatusInStock = 1 // 在库
|
||||||
|
DeviceStatusDistributed = 2 // 已分销
|
||||||
|
DeviceStatusActivated = 3 // 已激活
|
||||||
|
DeviceStatusSuspended = 4 // 已停用
|
||||||
|
)
|
||||||
|
|
||||||
|
// 号卡状态
|
||||||
|
const (
|
||||||
|
NumberCardStatusOnSale = 1 // 在售
|
||||||
|
NumberCardStatusOffSale = 2 // 下架
|
||||||
|
)
|
||||||
|
|
||||||
|
// IoT 卡激活状态
|
||||||
|
const (
|
||||||
|
ActivationStatusInactive = 0 // 未激活
|
||||||
|
ActivationStatusActive = 1 // 已激活
|
||||||
|
)
|
||||||
|
|
||||||
|
// IoT 卡实名状态
|
||||||
|
const (
|
||||||
|
RealNameStatusNotVerified = 0 // 未实名
|
||||||
|
RealNameStatusVerified = 1 // 已实名
|
||||||
|
)
|
||||||
|
|
||||||
|
// IoT 卡网络状态
|
||||||
|
const (
|
||||||
|
NetworkStatusOffline = 0 // 停机
|
||||||
|
NetworkStatusOnline = 1 // 开机
|
||||||
|
)
|
||||||
|
|
||||||
|
// 套餐流量类型
|
||||||
|
const (
|
||||||
|
DataTypeReal = "real" // 真流量
|
||||||
|
DataTypeVirtual = "virtual" // 虚流量
|
||||||
|
)
|
||||||
|
|
||||||
|
// 套餐类型
|
||||||
|
const (
|
||||||
|
PackageTypeFormal = "formal" // 正式套餐
|
||||||
|
PackageTypeAddon = "addon" // 附加套餐
|
||||||
|
)
|
||||||
|
|
||||||
|
// 订单类型
|
||||||
|
const (
|
||||||
|
OrderTypePackage = 1 // 套餐订单
|
||||||
|
OrderTypeNumberCard = 2 // 号卡订单
|
||||||
|
)
|
||||||
|
|
||||||
|
// 订单状态 (IoT 模块)
|
||||||
|
const (
|
||||||
|
IotOrderStatusPending = 1 // 待支付
|
||||||
|
IotOrderStatusPaid = 2 // 已支付
|
||||||
|
IotOrderStatusCompleted = 3 // 已完成
|
||||||
|
IotOrderStatusCancelled = 4 // 已取消
|
||||||
|
IotOrderStatusRefunded = 5 // 已退款
|
||||||
|
)
|
||||||
|
|
||||||
|
// 支付方式
|
||||||
|
const (
|
||||||
|
PaymentMethodWallet = "wallet" // 钱包
|
||||||
|
PaymentMethodOnline = "online" // 在线支付
|
||||||
|
PaymentMethodCarrier = "carrier" // 运营商支付
|
||||||
|
)
|
||||||
|
|
||||||
|
// 所有者类型
|
||||||
|
const (
|
||||||
|
OwnerTypePlatform = "platform" // 平台
|
||||||
|
OwnerTypeAgent = "agent" // 代理
|
||||||
|
OwnerTypeUser = "user" // 用户
|
||||||
|
OwnerTypeDevice = "device" // 设备
|
||||||
|
)
|
||||||
|
|
||||||
|
// 绑定状态
|
||||||
|
const (
|
||||||
|
BindStatusBound = 1 // 已绑定
|
||||||
|
BindStatusUnbound = 2 // 已解绑
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 2. 套餐和轮询相关常量
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// 套餐使用类型
|
||||||
|
const (
|
||||||
|
PackageUsageTypeSingleCard = "single_card" // 单卡套餐
|
||||||
|
PackageUsageTypeDevice = "device" // 设备级套餐
|
||||||
|
)
|
||||||
|
|
||||||
|
// 套餐使用状态
|
||||||
|
const (
|
||||||
|
PackageUsageStatusActive = 1 // 生效中
|
||||||
|
PackageUsageStatusExhausted = 2 // 已用完
|
||||||
|
PackageUsageStatusExpired = 3 // 已过期
|
||||||
|
)
|
||||||
|
|
||||||
|
// 轮询配置卡条件
|
||||||
|
const (
|
||||||
|
CardConditionNotRealName = "not_real_name" // 未实名
|
||||||
|
CardConditionRealName = "real_name" // 已实名
|
||||||
|
CardConditionActivated = "activated" // 已激活
|
||||||
|
CardConditionSuspended = "suspended" // 已停用
|
||||||
|
)
|
||||||
|
|
||||||
|
// 流量使用记录来源
|
||||||
|
const (
|
||||||
|
DataUsageSourcePolling = "polling" // 轮询
|
||||||
|
DataUsageSourceManual = "manual" // 手动
|
||||||
|
DataUsageSourceGateway = "gateway" // Gateway回调
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 3. 分佣相关常量
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// 分佣类型
|
||||||
|
const (
|
||||||
|
CommissionTypeOneTime = "one_time" // 一次性分佣
|
||||||
|
CommissionTypeLongTerm = "long_term" // 长期分佣
|
||||||
|
CommissionTypeCombined = "combined" // 组合分佣
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分佣模式
|
||||||
|
const (
|
||||||
|
CommissionModeFixed = "fixed" // 固定金额
|
||||||
|
CommissionModePercent = "percent" // 百分比
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分佣状态
|
||||||
|
const (
|
||||||
|
CommissionStatusFrozen = 1 // 已冻结
|
||||||
|
CommissionStatusUnfreezing = 2 // 解冻中
|
||||||
|
CommissionStatusReleased = 3 // 已发放
|
||||||
|
CommissionStatusInvalid = 4 // 已失效
|
||||||
|
)
|
||||||
|
|
||||||
|
// 阶梯类型
|
||||||
|
const (
|
||||||
|
LadderTypeActivation = "activation" // 激活量
|
||||||
|
LadderTypePickup = "pickup" // 提货量
|
||||||
|
LadderTypeDeposit = "deposit" // 充值量
|
||||||
|
)
|
||||||
|
|
||||||
|
// 卡类型
|
||||||
|
const (
|
||||||
|
CardTypeNumberCard = "number_card" // 号卡
|
||||||
|
CardTypeIotCard = "iot_card" // IoT卡
|
||||||
|
)
|
||||||
|
|
||||||
|
// 审批类型
|
||||||
|
const (
|
||||||
|
ApprovalTypeAuto = "auto" // 自动
|
||||||
|
ApprovalTypeManual = "manual" // 人工
|
||||||
|
)
|
||||||
|
|
||||||
|
// 审批状态
|
||||||
|
const (
|
||||||
|
ApprovalStatusPending = 1 // 待审批
|
||||||
|
ApprovalStatusApproved = 2 // 已通过
|
||||||
|
ApprovalStatusRejected = 3 // 已拒绝
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 4. 财务管理常量
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// 提现状态
|
||||||
|
const (
|
||||||
|
WithdrawalStatusPending = 1 // 待审核
|
||||||
|
WithdrawalStatusApproved = 2 // 已通过
|
||||||
|
WithdrawalStatusRejected = 3 // 已拒绝
|
||||||
|
WithdrawalStatusPaid = 4 // 已到账
|
||||||
|
)
|
||||||
|
|
||||||
|
// 提现方式
|
||||||
|
const (
|
||||||
|
WithdrawalMethodAlipay = "alipay" // 支付宝
|
||||||
|
WithdrawalMethodWechat = "wechat" // 微信
|
||||||
|
WithdrawalMethodBank = "bank" // 银行卡
|
||||||
|
)
|
||||||
|
|
||||||
|
// 商户类型
|
||||||
|
const (
|
||||||
|
MerchantTypeAlipay = "alipay" // 支付宝
|
||||||
|
MerchantTypeWechat = "wechat" // 微信
|
||||||
|
MerchantTypeBank = "bank" // 银行卡
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 5. 系统管理常量
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// 换卡申请状态
|
||||||
|
const (
|
||||||
|
ReplacementStatusPending = 1 // 待处理
|
||||||
|
ReplacementStatusApproved = 2 // 已通过
|
||||||
|
ReplacementStatusRejected = 3 // 已拒绝
|
||||||
|
ReplacementStatusCompleted = 4 // 已完成
|
||||||
|
)
|
||||||
|
|
||||||
|
// 开发能力配置状态
|
||||||
|
const (
|
||||||
|
DevCapabilityStatusEnabled = 1 // 启用
|
||||||
|
DevCapabilityStatusDisabled = 2 // 禁用
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 运营商编码
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
const (
|
||||||
|
CarrierCodeCMCC = "CMCC" // 中国移动
|
||||||
|
CarrierCodeCUCC = "CUCC" // 中国联通
|
||||||
|
CarrierCodeCTCC = "CTCC" // 中国电信
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user