This commit is contained in:
71
.agents/skills/ccc/SKILL.md
Normal file
71
.agents/skills/ccc/SKILL.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
name: ccc
|
||||||
|
description: "This skill should be used when code search is needed (whether explicitly requested or as part of completing a task), when indexing the codebase after changes, or when the user asks about ccc, cocoindex-code, or the codebase index. Trigger phrases include 'search the codebase', 'find code related to', 'update the index', 'ccc', 'cocoindex-code'."
|
||||||
|
---
|
||||||
|
|
||||||
|
# ccc - Semantic Code Search & Indexing
|
||||||
|
|
||||||
|
`ccc` is the CLI for CocoIndex Code, providing semantic search over the current codebase and index management.
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
The agent owns the `ccc` lifecycle for the current project — initialization, indexing, and searching. Do not ask the user to perform these steps; handle them automatically.
|
||||||
|
|
||||||
|
- **Initialization**: If `ccc search` or `ccc index` fails with an initialization error (e.g., "Not in an initialized project directory"), run `ccc init` from the project root directory, then `ccc index` to build the index, then retry the original command.
|
||||||
|
- **Index freshness**: Keep the index up to date by running `ccc index` (or `ccc search --refresh`) when the index may be stale — e.g., at the start of a session, or after making significant code changes (new files, refactors, renamed modules). There is no need to re-index between consecutive searches if no code was changed in between.
|
||||||
|
- **Installation**: If `ccc` itself is not found (command not found), refer to [management.md](references/management.md) for installation instructions and inform the user.
|
||||||
|
|
||||||
|
## Searching the Codebase
|
||||||
|
|
||||||
|
To perform a semantic search:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc search <query terms>
|
||||||
|
```
|
||||||
|
|
||||||
|
The query should describe the concept, functionality, or behavior to find, not exact code syntax. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc search database connection pooling
|
||||||
|
ccc search user authentication flow
|
||||||
|
ccc search error handling retry logic
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtering Results
|
||||||
|
|
||||||
|
- **By language** (`--lang`, repeatable): restrict results to specific languages.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc search --lang python --lang markdown database schema
|
||||||
|
```
|
||||||
|
|
||||||
|
- **By path** (`--path`): restrict results to a glob pattern relative to project root. If omitted, defaults to the current working directory (only results under that subdirectory are returned).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc search --path 'src/api/*' request validation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
Results default to the first page. To retrieve additional results:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc search --offset 5 --limit 5 database schema
|
||||||
|
```
|
||||||
|
|
||||||
|
If all returned results look relevant, use `--offset` to fetch the next page — there are likely more useful matches beyond the first page.
|
||||||
|
|
||||||
|
### Working with Search Results
|
||||||
|
|
||||||
|
Search results include file paths and line ranges. To explore a result in more detail:
|
||||||
|
|
||||||
|
- Use the editor's built-in file reading capabilities (e.g., the `Read` tool) to load the matched file and read lines around the returned range for full context.
|
||||||
|
- When working in a terminal without a file-reading tool, use `sed -n '<start>,<end>p' <file>` to extract a specific line range.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
To view or edit embedding model configuration, include/exclude patterns, or language overrides, see [settings.md](references/settings.md).
|
||||||
|
|
||||||
|
## Management & Troubleshooting
|
||||||
|
|
||||||
|
For installation, initialization, daemon management, troubleshooting, and cleanup commands, see [management.md](references/management.md).
|
||||||
110
.agents/skills/ccc/references/management.md
Normal file
110
.agents/skills/ccc/references/management.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# ccc Management
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install CocoIndex Code via pipx. Two install styles:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx install 'cocoindex-code[full]' # batteries included (local embeddings via sentence-transformers)
|
||||||
|
pipx install cocoindex-code # slim (LiteLLM-only; requires a cloud embedding provider + API key)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `[full]` extra pulls in `sentence-transformers` so the first-run default (local embeddings, no API key) works out of the box. The slim install is for environments where you don't want the torch/transformers deps and plan to use a LiteLLM-supported cloud provider instead.
|
||||||
|
|
||||||
|
To upgrade to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx upgrade cocoindex-code
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation, the `ccc` command is available globally.
|
||||||
|
|
||||||
|
## Project Initialization
|
||||||
|
|
||||||
|
Run from the root directory of the project to index:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc init
|
||||||
|
```
|
||||||
|
|
||||||
|
**First run (global settings don't exist yet)** — `ccc init` prompts interactively for the embedding provider (sentence-transformers / litellm) and model, then runs a one-off test embed via the daemon to confirm the model works. Accept the defaults for the sentence-transformers path, or pick litellm and enter a model identifier.
|
||||||
|
|
||||||
|
**Subsequent runs** (global settings already exist) — prompts are skipped; only project settings and `.gitignore` are set up.
|
||||||
|
|
||||||
|
To skip the interactive prompts on the first run (e.g. in a script or container), pass `--litellm-model MODEL`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc init --litellm-model openai/text-embedding-3-small
|
||||||
|
```
|
||||||
|
|
||||||
|
This is also the only way to pick a LiteLLM model when stdin isn't a TTY and you've done a slim install.
|
||||||
|
|
||||||
|
`ccc init` creates:
|
||||||
|
- `~/.cocoindex_code/global_settings.yml` (user-level, embedding config + env vars).
|
||||||
|
- `.cocoindex_code/settings.yml` (project-level, include/exclude patterns).
|
||||||
|
|
||||||
|
If `.git` exists in the directory, `.cocoindex_code/` is automatically added to `.gitignore`.
|
||||||
|
|
||||||
|
Use `-f` to skip the confirmation prompt if `ccc init` detects a potential parent project root.
|
||||||
|
|
||||||
|
After initialization, edit the settings files if needed (see [settings.md](settings.md) for format details), then run `ccc index` to build the initial index. If the model test printed `[FAIL]` during `init`, edit `global_settings.yml` (and optionally add API keys under the commented `envs:` block) and verify with `ccc doctor` before indexing.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Diagnostics
|
||||||
|
|
||||||
|
Run `ccc doctor` to check system health end-to-end:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks global settings, daemon status, embedding model (runs a test embedding), and — if run from within a project — file matching (walks files using the same logic as the indexer) and index status. Results stream incrementally. Always points to `daemon.log` at the end for further investigation.
|
||||||
|
|
||||||
|
### Checking Project Status
|
||||||
|
|
||||||
|
To view the current project's index status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc status
|
||||||
|
```
|
||||||
|
|
||||||
|
This shows whether indexing is ongoing and index statistics.
|
||||||
|
|
||||||
|
### Daemon Management
|
||||||
|
|
||||||
|
The daemon starts automatically on first use. To check its status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc daemon status
|
||||||
|
```
|
||||||
|
|
||||||
|
This shows whether the daemon is running, its version, uptime, and loaded projects.
|
||||||
|
|
||||||
|
To restart the daemon (useful if it gets into a bad state):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc daemon restart
|
||||||
|
```
|
||||||
|
|
||||||
|
To stop the daemon:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc daemon stop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
To reset a project's index (removes databases, keeps settings):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc reset
|
||||||
|
```
|
||||||
|
|
||||||
|
To fully remove all CocoIndex Code data for a project (including settings):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc reset --all
|
||||||
|
```
|
||||||
|
|
||||||
|
Both commands prompt for confirmation. Use `-f` to skip.
|
||||||
126
.agents/skills/ccc/references/settings.md
Normal file
126
.agents/skills/ccc/references/settings.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# ccc Settings
|
||||||
|
|
||||||
|
Configuration lives in two YAML files, both created automatically by `ccc init`.
|
||||||
|
|
||||||
|
## User-Level Settings (`~/.cocoindex_code/global_settings.yml`)
|
||||||
|
|
||||||
|
Shared across all projects. Controls the embedding model and extra environment variables for the daemon.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
embedding:
|
||||||
|
provider: sentence-transformers # or "litellm" (default when provider is omitted)
|
||||||
|
model: Snowflake/snowflake-arctic-embed-xs
|
||||||
|
device: mps # optional: cpu, cuda, mps (auto-detected if omitted)
|
||||||
|
min_interval_ms: 300 # optional: pace LiteLLM embedding requests to reduce 429s; defaults to 5 for LiteLLM
|
||||||
|
|
||||||
|
envs: # extra environment variables for the daemon
|
||||||
|
OPENAI_API_KEY: your-key # only needed if not already in the shell environment
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `embedding.provider` | `sentence-transformers` for local models, `litellm` (or omit) for cloud/remote models |
|
||||||
|
| `embedding.model` | Model identifier — format depends on provider (see examples below) |
|
||||||
|
| `embedding.device` | Optional. `cpu`, `cuda`, or `mps`. Auto-detected if omitted. Only relevant for `sentence-transformers`. |
|
||||||
|
| `embedding.min_interval_ms` | Optional. Minimum delay between LiteLLM embedding requests in milliseconds. Defaults to `5` for LiteLLM and is ignored by `sentence-transformers`. Set explicitly to override the default. |
|
||||||
|
| `envs` | Key-value map of environment variables injected into the daemon. Use for API keys not already in the shell environment. |
|
||||||
|
|
||||||
|
### Embedding Model Examples
|
||||||
|
|
||||||
|
**Local (sentence-transformers, no API key needed):**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
embedding:
|
||||||
|
provider: sentence-transformers
|
||||||
|
model: Snowflake/snowflake-arctic-embed-xs # default, lightweight
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
embedding:
|
||||||
|
provider: sentence-transformers
|
||||||
|
model: nomic-ai/CodeRankEmbed # better code retrieval, needs GPU (~1 GB VRAM)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ollama (local):**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
embedding:
|
||||||
|
model: ollama/nomic-embed-text
|
||||||
|
```
|
||||||
|
|
||||||
|
**OpenAI:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
embedding:
|
||||||
|
model: text-embedding-3-small
|
||||||
|
min_interval_ms: 300
|
||||||
|
envs:
|
||||||
|
OPENAI_API_KEY: your-api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gemini:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
embedding:
|
||||||
|
model: gemini/gemini-embedding-001
|
||||||
|
envs:
|
||||||
|
GEMINI_API_KEY: your-api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
**Voyage (code-optimized):**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
embedding:
|
||||||
|
model: voyage/voyage-code-3
|
||||||
|
envs:
|
||||||
|
VOYAGE_API_KEY: your-api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
For the full list of supported cloud providers and model identifiers, see [LiteLLM Embedding Models](https://docs.litellm.ai/docs/embedding/supported_embedding).
|
||||||
|
|
||||||
|
### Important
|
||||||
|
|
||||||
|
Switching embedding models changes vector dimensions — you must re-index after changing the model:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ccc reset && ccc index
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project-Level Settings (`<project>/.cocoindex_code/settings.yml`)
|
||||||
|
|
||||||
|
Per-project. Controls which files to index. Created by `ccc init` and automatically added to `.gitignore`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
include_patterns:
|
||||||
|
- "**/*.py"
|
||||||
|
- "**/*.js"
|
||||||
|
- "**/*.ts"
|
||||||
|
# ... (sensible defaults for 28+ file types)
|
||||||
|
|
||||||
|
exclude_patterns:
|
||||||
|
- "**/.*" # hidden directories
|
||||||
|
- "**/__pycache__"
|
||||||
|
- "**/node_modules"
|
||||||
|
- "**/dist"
|
||||||
|
# ...
|
||||||
|
|
||||||
|
language_overrides:
|
||||||
|
- ext: inc # treat .inc files as PHP
|
||||||
|
lang: php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `include_patterns` | Glob patterns for files to index. Defaults cover common languages (Python, JS/TS, Rust, Go, Java, C/C++, C#, SQL, Shell, Markdown, PHP, Lua, etc.). |
|
||||||
|
| `exclude_patterns` | Glob patterns for files/directories to skip. Defaults exclude hidden dirs, `node_modules`, `dist`, `__pycache__`, `vendor`, etc. |
|
||||||
|
| `language_overrides` | List of `{ext, lang}` pairs to override language detection for specific file extensions. |
|
||||||
|
|
||||||
|
### Editing Tips
|
||||||
|
|
||||||
|
- To index additional file types, append glob patterns to `include_patterns` (e.g. `"**/*.proto"`).
|
||||||
|
- To exclude a directory, append to `exclude_patterns` (e.g. `"**/generated"`).
|
||||||
|
- After editing, run `ccc index` to re-index with the new settings.
|
||||||
1
.claude/skills/ccc
Symbolic link
1
.claude/skills/ccc
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/ccc
|
||||||
@@ -933,13 +933,16 @@ components:
|
|||||||
$ref: '#/components/schemas/DtoBoundCardInfo'
|
$ref: '#/components/schemas/DtoBoundCardInfo'
|
||||||
type: array
|
type: array
|
||||||
current_month_usage_mb:
|
current_month_usage_mb:
|
||||||
description: 系统累计的自然月流量MB(asset_type=card时有效)
|
description: 系统自然月累计流量(MB,asset_type=card时有效,不等于运营商周期口径)
|
||||||
type: number
|
type: number
|
||||||
device_protect_status:
|
device_protect_status:
|
||||||
description: 保护期状态(asset_type=device时有效):none/stop/start
|
description: 保护期状态(asset_type=device时有效):none/stop/start
|
||||||
type: string
|
type: string
|
||||||
device_realtime:
|
device_realtime:
|
||||||
$ref: '#/components/schemas/DtoDeviceGatewayInfo'
|
$ref: '#/components/schemas/DtoDeviceGatewayInfo'
|
||||||
|
last_gateway_reading_mb:
|
||||||
|
description: 运营商当前周期累计流量读数(MB,asset_type=card时有效,运营商口径展示建议读取该字段)
|
||||||
|
type: number
|
||||||
last_sync_time:
|
last_sync_time:
|
||||||
description: 最后同步时间(asset_type=card时有效)
|
description: 最后同步时间(asset_type=card时有效)
|
||||||
format: date-time
|
format: date-time
|
||||||
@@ -3226,6 +3229,9 @@ components:
|
|||||||
msisdn:
|
msisdn:
|
||||||
description: 接入号
|
description: 接入号
|
||||||
type: string
|
type: string
|
||||||
|
network_status:
|
||||||
|
description: 网络状态:0停机 1开机(asset_type=card时有效)
|
||||||
|
type: integer
|
||||||
realname_policy:
|
realname_policy:
|
||||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||||
type: string
|
type: string
|
||||||
@@ -4370,7 +4376,7 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
realname_policy:
|
realname_policy:
|
||||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名),默认 none
|
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名),默认 after_order
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- file_key
|
- file_key
|
||||||
@@ -4407,7 +4413,7 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
realname_policy:
|
realname_policy:
|
||||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名),默认 none
|
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名),默认 after_order
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- carrier_id
|
- carrier_id
|
||||||
@@ -6871,7 +6877,7 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
type: string
|
type: string
|
||||||
current_month_usage_mb:
|
current_month_usage_mb:
|
||||||
description: 本月已用流量(MB)
|
description: 系统自然月累计流量(MB,不等于运营商周期口径)
|
||||||
type: number
|
type: number
|
||||||
data_usage_mb:
|
data_usage_mb:
|
||||||
description: 累计流量使用(MB)
|
description: 累计流量使用(MB)
|
||||||
@@ -6897,6 +6903,9 @@ components:
|
|||||||
format: date-time
|
format: date-time
|
||||||
nullable: true
|
nullable: true
|
||||||
type: string
|
type: string
|
||||||
|
last_gateway_reading_mb:
|
||||||
|
description: 运营商当前周期累计流量读数(MB,展示运营商口径时使用)
|
||||||
|
type: number
|
||||||
last_month_total_mb:
|
last_month_total_mb:
|
||||||
description: 上月流量总量(MB)
|
description: 上月流量总量(MB)
|
||||||
type: number
|
type: number
|
||||||
@@ -9610,14 +9619,38 @@ paths:
|
|||||||
- 资产管理
|
- 资产管理
|
||||||
/api/admin/assets/{identifier}/packages:
|
/api/admin/assets/{identifier}/packages:
|
||||||
get:
|
get:
|
||||||
description: 查询该资产所有套餐记录,含虚流量换算结果。支持分页。
|
description: 查询该资产所有套餐记录,含虚流量换算结果。支持分页与 status 状态筛选。
|
||||||
parameters:
|
parameters:
|
||||||
- description: 资产标识符(ICCID 或 VirtualNo)
|
- description: 页码(默认1)
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
schema:
|
||||||
|
description: 页码(默认1)
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
- description: 每页条数(默认50,最大100)
|
||||||
|
in: query
|
||||||
|
name: page_size
|
||||||
|
schema:
|
||||||
|
description: 每页条数(默认50,最大100)
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
- description: 套餐状态 (0:待生效, 1:生效中, 2:已用完, 3:已过期, 4:已失效)
|
||||||
|
in: query
|
||||||
|
name: status
|
||||||
|
schema:
|
||||||
|
description: 套餐状态 (0:待生效, 1:生效中, 2:已用完, 3:已过期, 4:已失效)
|
||||||
|
maximum: 4
|
||||||
|
minimum: 0
|
||||||
|
nullable: true
|
||||||
|
type: integer
|
||||||
|
- description: 资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN)
|
||||||
in: path
|
in: path
|
||||||
name: identifier
|
name: identifier
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
description: 资产标识符(ICCID 或 VirtualNo)
|
description: 资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN)
|
||||||
type: string
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
@@ -15023,6 +15056,13 @@ paths:
|
|||||||
description: ICCID结束号
|
description: ICCID结束号
|
||||||
maxLength: 20
|
maxLength: 20
|
||||||
type: string
|
type: string
|
||||||
|
- description: 运营商名称(模糊查询)
|
||||||
|
in: query
|
||||||
|
name: carrier_name
|
||||||
|
schema:
|
||||||
|
description: 运营商名称(模糊查询)
|
||||||
|
maxLength: 100
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
content:
|
content:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type OrderListRequest struct {
|
|||||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||||
PaymentStatus *int `json:"payment_status" query:"payment_status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"支付状态 (1:待支付, 2:已支付, 3:已取消, 4:已退款)"`
|
PaymentStatus *int `json:"payment_status" query:"payment_status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"支付状态 (1:待支付, 2:已支付, 3:已取消, 4:已退款)"`
|
||||||
|
PaymentMethod string `json:"payment_method" query:"payment_method" validate:"omitempty,oneof=wallet wechat alipay offline" description:"支付方式 (wallet:钱包支付, wechat:微信支付, alipay:支付宝支付, offline:线下支付)"`
|
||||||
OrderType string `json:"order_type" query:"order_type" validate:"omitempty,oneof=single_card device" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
|
OrderType string `json:"order_type" query:"order_type" validate:"omitempty,oneof=single_card device" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
|
||||||
OrderNo string `json:"order_no" query:"order_no" validate:"omitempty,max=30" maxLength:"30" description:"订单号(精确查询)"`
|
OrderNo string `json:"order_no" query:"order_no" validate:"omitempty,max=30" maxLength:"30" description:"订单号(精确查询)"`
|
||||||
PurchaseRole string `json:"purchase_role" query:"purchase_role" validate:"omitempty,oneof=self_purchase purchased_by_parent purchased_by_platform purchase_for_subordinate" description:"订单角色 (self_purchase:自己购买, purchased_by_parent:上级代理购买, purchased_by_platform:平台代购, purchase_for_subordinate:给下级购买)"`
|
PurchaseRole string `json:"purchase_role" query:"purchase_role" validate:"omitempty,oneof=self_purchase purchased_by_parent purchased_by_platform purchase_for_subordinate" description:"订单角色 (self_purchase:自己购买, purchased_by_parent:上级代理购买, purchased_by_platform:平台代购, purchase_for_subordinate:给下级购买)"`
|
||||||
|
|||||||
@@ -339,6 +339,11 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
|
|||||||
return nil, errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付")
|
return nil, errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if paymentStatus == model.PaymentStatusPaid && actualPaidAmount == nil {
|
||||||
|
actualPaidAmountSnapshot := totalAmount
|
||||||
|
actualPaidAmount = &actualPaidAmountSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
// 查询当前生效的支付配置
|
// 查询当前生效的支付配置
|
||||||
var paymentConfigID *uint
|
var paymentConfigID *uint
|
||||||
if req.PaymentMethod == model.PaymentMethodWechat || req.PaymentMethod == model.PaymentMethodAlipay {
|
if req.PaymentMethod == model.PaymentMethodWechat || req.PaymentMethod == model.PaymentMethodAlipay {
|
||||||
@@ -608,6 +613,11 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if paymentStatus == model.PaymentStatusPaid && actualPaidAmount == nil {
|
||||||
|
actualPaidAmountSnapshot := totalAmount
|
||||||
|
actualPaidAmount = &actualPaidAmountSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
// 查询当前生效的支付配置
|
// 查询当前生效的支付配置
|
||||||
var h5PaymentConfigID *uint
|
var h5PaymentConfigID *uint
|
||||||
if req.PaymentMethod == model.PaymentMethodWechat || req.PaymentMethod == model.PaymentMethodAlipay {
|
if req.PaymentMethod == model.PaymentMethodWechat || req.PaymentMethod == model.PaymentMethodAlipay {
|
||||||
@@ -998,6 +1008,9 @@ func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType
|
|||||||
if req.PaymentStatus != nil {
|
if req.PaymentStatus != nil {
|
||||||
filters["payment_status"] = *req.PaymentStatus
|
filters["payment_status"] = *req.PaymentStatus
|
||||||
}
|
}
|
||||||
|
if req.PaymentMethod != "" {
|
||||||
|
filters["payment_method"] = req.PaymentMethod
|
||||||
|
}
|
||||||
if req.OrderType != "" {
|
if req.OrderType != "" {
|
||||||
filters["order_type"] = req.OrderType
|
filters["order_type"] = req.OrderType
|
||||||
}
|
}
|
||||||
@@ -1247,6 +1260,7 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
|||||||
|
|
||||||
// 根据资源类型选择对应的钱包系统
|
// 根据资源类型选择对应的钱包系统
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
actualPaidAmount := order.TotalAmount
|
||||||
|
|
||||||
if resourceType == "shop" {
|
if resourceType == "shop" {
|
||||||
// 代理钱包系统(店铺)
|
// 代理钱包系统(店铺)
|
||||||
@@ -1266,10 +1280,11 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
|||||||
result := tx.Model(&model.Order{}).
|
result := tx.Model(&model.Order{}).
|
||||||
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
|
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
"payment_status": model.PaymentStatusPaid,
|
"payment_status": model.PaymentStatusPaid,
|
||||||
"payment_method": model.PaymentMethodWallet,
|
"payment_method": model.PaymentMethodWallet,
|
||||||
"paid_at": now,
|
"paid_at": now,
|
||||||
"expires_at": nil, // 支付成功,清除过期时间
|
"expires_at": nil, // 支付成功,清除过期时间
|
||||||
|
"actual_paid_amount": actualPaidAmount,
|
||||||
})
|
})
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
|
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
|
||||||
@@ -1293,6 +1308,9 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actualPaidAmountSnapshot := actualPaidAmount
|
||||||
|
order.ActualPaidAmount = &actualPaidAmountSnapshot
|
||||||
|
|
||||||
walletResult := tx.Model(&model.AgentWallet{}).
|
walletResult := tx.Model(&model.AgentWallet{}).
|
||||||
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
|
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
@@ -1330,10 +1348,11 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
|||||||
result := tx.Model(&model.Order{}).
|
result := tx.Model(&model.Order{}).
|
||||||
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
|
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
"payment_status": model.PaymentStatusPaid,
|
"payment_status": model.PaymentStatusPaid,
|
||||||
"payment_method": model.PaymentMethodWallet,
|
"payment_method": model.PaymentMethodWallet,
|
||||||
"paid_at": now,
|
"paid_at": now,
|
||||||
"expires_at": nil, // 支付成功,清除过期时间
|
"expires_at": nil, // 支付成功,清除过期时间
|
||||||
|
"actual_paid_amount": actualPaidAmount,
|
||||||
})
|
})
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
|
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
|
||||||
@@ -1357,6 +1376,9 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actualPaidAmountSnapshot := actualPaidAmount
|
||||||
|
order.ActualPaidAmount = &actualPaidAmountSnapshot
|
||||||
|
|
||||||
// 扣款前记录余额快照,用于写入流水
|
// 扣款前记录余额快照,用于写入流水
|
||||||
balanceBefore := wallet.Balance
|
balanceBefore := wallet.Balance
|
||||||
|
|
||||||
@@ -2030,6 +2052,12 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
|||||||
assetType = "device"
|
assetType = "device"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actualPaidAmount := order.ActualPaidAmount
|
||||||
|
if actualPaidAmount == nil && order.PaymentStatus == model.PaymentStatusPaid {
|
||||||
|
actualPaidAmountSnapshot := order.TotalAmount
|
||||||
|
actualPaidAmount = &actualPaidAmountSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
return &dto.OrderResponse{
|
return &dto.OrderResponse{
|
||||||
ID: order.ID,
|
ID: order.ID,
|
||||||
OrderNo: order.OrderNo,
|
OrderNo: order.OrderNo,
|
||||||
@@ -2054,7 +2082,7 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
|||||||
OperatorID: order.OperatorID,
|
OperatorID: order.OperatorID,
|
||||||
OperatorType: order.OperatorType,
|
OperatorType: order.OperatorType,
|
||||||
OperatorName: operatorName,
|
OperatorName: operatorName,
|
||||||
ActualPaidAmount: order.ActualPaidAmount,
|
ActualPaidAmount: actualPaidAmount,
|
||||||
|
|
||||||
PurchaseRole: order.PurchaseRole,
|
PurchaseRole: order.PurchaseRole,
|
||||||
IsPurchasedByParent: isPurchasedByParent,
|
IsPurchasedByParent: isPurchasedByParent,
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters
|
|||||||
if v, ok := filters["order_type"]; ok {
|
if v, ok := filters["order_type"]; ok {
|
||||||
query = query.Where("order_type = ?", v)
|
query = query.Where("order_type = ?", v)
|
||||||
}
|
}
|
||||||
|
if v, ok := filters["payment_method"]; ok {
|
||||||
|
query = query.Where("payment_method = ?", v)
|
||||||
|
}
|
||||||
if v, ok := filters["order_no"]; ok {
|
if v, ok := filters["order_no"]; ok {
|
||||||
query = query.Where("order_no = ?", v)
|
query = query.Where("order_no = ?", v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-04-22
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 修复前基线证据(任务 1.1 / 1.2)
|
||||||
|
|
||||||
|
## 采样时间
|
||||||
|
|
||||||
|
- 2026-04-22 18:00(Asia/Shanghai)
|
||||||
|
|
||||||
|
## 样本说明
|
||||||
|
|
||||||
|
- 采样条件:
|
||||||
|
- 卡已绑定设备(`tb_device_sim_binding.bind_status=1`)
|
||||||
|
- 该卡无生效卡级套餐(`tb_package_usage.iot_card_id` 无 `status=1`)
|
||||||
|
- 所属设备存在生效设备级套餐(`tb_package_usage.device_id` 有 `status=1`)
|
||||||
|
|
||||||
|
## 关键基线数据(MCP 查询)
|
||||||
|
|
||||||
|
### 1) 绑定设备卡流量已增长
|
||||||
|
|
||||||
|
| card_id | iccid | device_id | last_gateway_reading_mb | current_month_usage_mb | card_usage_mb | last_data_check_at |
|
||||||
|
|---|---|---:|---:|---:|---:|---|
|
||||||
|
| 4 | 89860885192590572650 | 2 | 2875.72 | 2875.72 | 2872 | 2026-04-22T09:44:22.355Z |
|
||||||
|
| 6 | 8986062575000172349 | 2 | 9047.27 | 9047.27 | 9047 | 2026-04-22T09:48:28.203Z |
|
||||||
|
| 7 | 89860624660021733454 | 3 | 33137.81 | 33137.81 | 33136 | 2026-04-22T09:48:50.758Z |
|
||||||
|
|
||||||
|
### 2) 对应设备级套餐未扣减
|
||||||
|
|
||||||
|
| device_id | package_usage_id | usage_type | status | data_usage_mb | package_updated_at |
|
||||||
|
|---:|---:|---|---:|---:|---|
|
||||||
|
| 2 | 5 | device | 1 | 0 | 2026-04-22T07:19:38.584Z |
|
||||||
|
| 3 | 6 | device | 1 | 0 | 2026-04-22T08:24:56.831Z |
|
||||||
|
|
||||||
|
## 基线结论
|
||||||
|
|
||||||
|
- 卡侧流量字段(`last_gateway_reading_mb` / `current_month_usage_mb` / `tb_iot_card.data_usage_mb`)已明显增长。
|
||||||
|
- 设备级生效套餐(`tb_package_usage.status=1` 且 `usage_type=device`)的 `data_usage_mb` 仍为 0。
|
||||||
|
- 可确认当前存在“卡流量增长但套餐未扣减”的问题基线,符合本提案修复目标。
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
当前 `polling_carddata` 链路已经能正确更新卡维度流量读数,但套餐扣减存在两个缺口:
|
||||||
|
|
||||||
|
1. 载体路由缺口:扣减调用固定使用 `carrier_type=iot_card`,导致绑定设备且套餐挂在 `device_id` 时无法命中可扣套餐。
|
||||||
|
2. 精度缺口:上游流量读数是 `float64 MB`,扣减时转 `int64`,会丢失 `<1MB` 的高频增量。
|
||||||
|
|
||||||
|
系统现有分层为 Handler → Service → Store → Model,扣减核心在 `UsageService.DeductDataUsage`,轮询和手动刷新都依赖该能力。为避免分叉逻辑,本次设计将修复集中在 Service 层并保持调用链兼容。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- 修复绑定设备卡的套餐扣减命中问题,保证设备级套餐能被正确扣减。
|
||||||
|
- 修复小数增量丢失问题,保证高频小流量场景下长期扣减准确。
|
||||||
|
- 明确流量字段语义:`current_month_usage_mb` 与运营商周期口径解耦。
|
||||||
|
- 保持现有 API、数据库表结构和任务调度不破坏。
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 不重构轮询体系,不调整 Asynq 任务类型。
|
||||||
|
- 不新增数据库迁移。
|
||||||
|
- 不改变套餐优先级策略(仍是加油包优先、主套餐兜底)。
|
||||||
|
- 不新增自动化测试范围(按项目约束,仅提供手动验证方案)。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 决策 1:载体命中策略下沉到 UsageService
|
||||||
|
|
||||||
|
- 方案:`DeductDataUsage` 先按请求载体查询生效套餐;若 `iot_card` 未命中且该卡存在有效设备绑定,则回退按 `device` 查询并扣减。
|
||||||
|
- 理由:
|
||||||
|
- 轮询与手动刷新共用一套扣减逻辑,避免在多个 Handler/Service 重复实现路由判断。
|
||||||
|
- 符合分层约束,业务判断集中在 Service 层,Store 层保持通用查询职责。
|
||||||
|
- 备选方案:在 `polling_carddata_handler` 单点改传 `device`。
|
||||||
|
- 未采用原因:手动刷新链路仍会错;后续新入口也会重复踩坑。
|
||||||
|
|
||||||
|
### 决策 2:小数增量使用“余量累加”而非直接四舍五入
|
||||||
|
|
||||||
|
- 方案:保留扣减单位为整数 MB,但把小数部分按载体维度写 Redis 余量键(带 TTL);后续增量先与余量相加,再计算本次可扣整数 MB。
|
||||||
|
- 理由:
|
||||||
|
- 不改变 `tb_package_usage.data_usage_mb` 的整数结构,兼容现有统计与查询。
|
||||||
|
- 避免每次四舍五入带来的长期偏差。
|
||||||
|
- Redis 读写轻量,满足轮询高频场景性能要求。
|
||||||
|
- 备选方案:直接把 `data_usage_mb` 改为小数。
|
||||||
|
- 未采用原因:涉及模型、查询、统计口径和历史数据兼容,变更面过大。
|
||||||
|
|
||||||
|
### 决策 3:字段口径保持双轨,不再混用
|
||||||
|
|
||||||
|
- 方案:
|
||||||
|
- `current_month_usage_mb`:继续定义为“系统自然月累计(卡维度)”。
|
||||||
|
- `last_gateway_reading_mb`:用于表达“运营商当前周期累计读数(卡维度)”。
|
||||||
|
- 理由:
|
||||||
|
- 运营商重置日可能不是每月 1 号,不能用自然月字段替代运营商周期口径。
|
||||||
|
- 绑定设备不改变流量采集主体,流量仍来自卡 ICCID。
|
||||||
|
- 备选方案:让 `current_month_usage_mb` 直接改为运营商周期口径。
|
||||||
|
- 未采用原因:会破坏现有自然月统计语义,影响已有展示与分析逻辑。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- 风险:余量键读写失败时会影响小数累计精度。
|
||||||
|
- Mitigation:失败仅降级为“本次不累计小数”,并记录告警日志,主流程不中断。
|
||||||
|
|
||||||
|
- 风险:`iot_card -> device` 回退可能在极端并发下出现重复判断。
|
||||||
|
- Mitigation:扣减仍在事务内按当前生效套餐计算,保持幂等结果。
|
||||||
|
|
||||||
|
- 风险:前端继续误用 `current_month_usage_mb` 作为运营商周期口径。
|
||||||
|
- Mitigation:在 spec 和接口描述中显式标注口径,前端改读 `last_gateway_reading_mb`。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. 先上线 Service 层扣减修复(载体回退 + 小数余量)。
|
||||||
|
2. 通过日志与数据库手动核对重点卡(绑定设备卡、低流量高频卡)。
|
||||||
|
3. 前端切换运营商周期展示口径到 `last_gateway_reading_mb`。
|
||||||
|
4. 观察 1-2 个运营商结算周期,无异常后关闭问题。
|
||||||
|
|
||||||
|
回滚策略:
|
||||||
|
- 若出现异常,可回滚到旧版本代码;该变更无 DB schema 迁移,不涉及数据结构回滚。
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- 余量 Redis 键 TTL 的最终值(建议 7 天)是否需要按运营商周期动态设置。
|
||||||
|
- 是否需要在管理端 realtime-status 直接返回 `last_gateway_reading_mb`(当前先由前端按现有可用字段接入)。
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
当前线上存在“卡流量已同步但套餐流量不变化”的问题,导致计费与停复机判断失真。该问题集中出现在绑定设备的卡和小流量高频上报场景,已经影响流量扣减准确性,需要优先修复。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- feature-001-traffic-deduction-carrier-routing:修正轮询流量扣减时的载体路由逻辑,避免固定按 `iot_card` 扣减导致设备级套餐无法命中。
|
||||||
|
- feature-002-traffic-deduction-fractional-precision:修正流量扣减精度,消除 `float64 -> int64` 截断造成的 `<1MB` 增量长期丢失。
|
||||||
|
- feature-003-traffic-field-semantics-clarify:明确字段语义与展示口径:
|
||||||
|
- `current_month_usage_mb`:系统自然月累计流量(卡维度)
|
||||||
|
- `last_gateway_reading_mb`:运营商当前周期累计读数(卡维度)
|
||||||
|
- 保持现有分层架构与任务链路不变(Handler → Service → Store → Model),仅调整扣减判定与数据口径说明;无 BREAKING API 变更。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `polling-task-handlers`:`polling_carddata_handler` 在触发套餐扣减时,需支持“卡绑定设备时按设备载体命中套餐”而非固定卡载体。
|
||||||
|
- `package-usage-priority`:`DeductDataUsage` 需支持小数 MB 增量累计后扣减,保证小流量高频上报场景下扣减准确。
|
||||||
|
- `asset-queries`:资产流量字段语义说明补充,避免将 `current_month_usage_mb` 误用为运营商周期口径。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- 受影响代码:
|
||||||
|
- `internal/task/polling_carddata_handler.go`
|
||||||
|
- `internal/service/package/usage_service.go`
|
||||||
|
- `internal/service/iot_card/service.go`
|
||||||
|
- `internal/model/dto/asset_dto.go`(描述语义)
|
||||||
|
- 受影响数据:
|
||||||
|
- `tb_package_usage.data_usage_mb` 的增长将更贴近上游真实增量(尤其是 <1MB 高频增量场景)
|
||||||
|
- API 兼容性:
|
||||||
|
- 无接口路径和结构变更
|
||||||
|
- 前端展示口径建议改为读取 `last_gateway_reading_mb` 表达“运营商周期累计流量”
|
||||||
|
- 依赖与基础设施:无新增外部依赖、无数据库迁移。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 卡流量字段口径必须明确且不可混用
|
||||||
|
系统对卡维度流量字段 SHALL 保持明确语义:
|
||||||
|
- `current_month_usage_mb`:系统自然月累计流量;
|
||||||
|
- `last_gateway_reading_mb`:运营商当前周期累计读数。
|
||||||
|
|
||||||
|
#### Scenario: 绑定设备不改变卡流量采集主体
|
||||||
|
- **WHEN** 卡已绑定设备并执行流量同步
|
||||||
|
- **THEN** 该卡流量字段仍按卡 ICCID 维度更新,不得被解释为设备聚合流量
|
||||||
|
|
||||||
|
#### Scenario: 运营商周期展示口径
|
||||||
|
- **WHEN** 前端需要展示“运营商周期累计流量”
|
||||||
|
- **THEN** 应使用 `last_gateway_reading_mb` 口径,不得使用 `current_month_usage_mb` 代替
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 流量扣减必须保留小数增量精度
|
||||||
|
系统处理上游流量增量时 SHALL 支持小数 MB 累计,不得因 `float64 -> int64` 截断导致长期漏扣。
|
||||||
|
|
||||||
|
#### Scenario: 多次小于 1MB 增量累计后触发扣减
|
||||||
|
- **WHEN** 同一载体连续收到 0.4MB、0.3MB、0.5MB 的增量
|
||||||
|
- **THEN** 系统累计小数余量后至少完成 1MB 套餐扣减,剩余小数继续保留
|
||||||
|
|
||||||
|
#### Scenario: 单次小数增量不足 1MB
|
||||||
|
- **WHEN** 本次增量为 0.2MB 且累计后仍小于 1MB
|
||||||
|
- **THEN** 系统不执行套餐 `data_usage_mb` 整数扣减,但必须保留该小数余量用于后续累计
|
||||||
|
|
||||||
|
#### Scenario: 轮询与手动刷新口径一致
|
||||||
|
- **WHEN** 流量增量分别来自轮询任务和手动刷新
|
||||||
|
- **THEN** 两条链路使用一致的小数累计扣减规则,不能出现一条链路扣减、另一条链路漏扣
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 流量轮询扣减必须支持设备载体回退
|
||||||
|
系统在 `polling_carddata_handler` 触发套餐扣减时 SHALL 支持载体自动命中:
|
||||||
|
- 优先按 `iot_card` 载体扣减;
|
||||||
|
- 当卡载体无生效套餐且该卡存在有效设备绑定时,回退按 `device` 载体扣减。
|
||||||
|
|
||||||
|
#### Scenario: 绑定设备卡命中设备级套餐
|
||||||
|
- **WHEN** 轮询检测到卡有正向流量增量,且该卡 `iot_card_id` 下无生效套餐、但其 `device_id` 下有生效套餐
|
||||||
|
- **THEN** 系统使用 `device` 载体执行扣减,套餐 `data_usage_mb` 正常增长
|
||||||
|
|
||||||
|
#### Scenario: 独立卡保持卡载体扣减
|
||||||
|
- **WHEN** 轮询检测到独立卡有正向流量增量
|
||||||
|
- **THEN** 系统继续按 `iot_card` 载体扣减,不引入设备载体分支
|
||||||
|
|
||||||
|
#### Scenario: 卡与设备都无生效套餐
|
||||||
|
- **WHEN** 轮询检测到流量增量,但卡载体与回退设备载体均无可用套餐
|
||||||
|
- **THEN** 系统记录“无可用套餐”并保持任务链路可继续重入队,不得中断轮询主流程
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## 1. 基线确认
|
||||||
|
|
||||||
|
- [x] 1.1 用目标卡/设备样本确认当前问题基线:卡流量增长但套餐 `data_usage_mb` 不增长
|
||||||
|
- [x] 1.2 记录基线证据(关键日志、`tb_iot_card` 与 `tb_package_usage` 当前值)用于修复后对比
|
||||||
|
|
||||||
|
## 2. 载体路由修复
|
||||||
|
|
||||||
|
- [x] 2.1 调整流量扣减链路,确保扣减入口支持“`iot_card` 未命中时回退 `device` 载体”
|
||||||
|
- [x] 2.2 在 `UsageService` 内统一实现载体命中逻辑,避免轮询与手动刷新分叉
|
||||||
|
- [x] 2.3 手动验证绑定设备卡场景:设备级套餐 `data_usage_mb` 可随流量同步增长
|
||||||
|
|
||||||
|
## 3. 小数流量精度修复
|
||||||
|
|
||||||
|
- [x] 3.1 调整扣减入参与内部计算口径,支持 `float64` 增量处理
|
||||||
|
- [x] 3.2 增加“流量小数余量”Redis 键与读写逻辑,累计后再执行整数 MB 扣减
|
||||||
|
- [x] 3.3 手动验证 `<1MB` 高频增量场景:多次同步后套餐扣减累计值正确
|
||||||
|
|
||||||
|
## 4. 字段语义对齐
|
||||||
|
|
||||||
|
- [x] 4.1 更新相关 DTO/文档描述,明确 `current_month_usage_mb` 是自然月口径
|
||||||
|
- [x] 4.2 在文档中明确 `last_gateway_reading_mb` 用于运营商周期累计口径展示
|
||||||
|
|
||||||
|
## 5. 回归与验收
|
||||||
|
|
||||||
|
- [x] 5.1 手动回归轮询与手动刷新两条链路,确认扣减逻辑一致
|
||||||
|
- [x] 5.2 用 PostgreSQL MCP 复核关键数据:`tb_iot_card.last_gateway_reading_mb` 与 `tb_package_usage.data_usage_mb` 变化符合预期
|
||||||
|
- [x] 5.3 执行 `openspec status --change fix-traffic-deduction-on-device-card`,确认提案进入可执行状态
|
||||||
|
|
||||||
|
> 说明:2.3、3.3、5.1、5.2 由用户手动验证并确认通过。
|
||||||
10
skills-lock.json
Normal file
10
skills-lock.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"ccc": {
|
||||||
|
"source": "cocoindex-io/cocoindex-code",
|
||||||
|
"sourceType": "github",
|
||||||
|
"computedHash": "485211ec7ef033f784261d1dba56770fea20163aebdd482950e8d4878b74c6b2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user