feat: 添加客户订单管理模块,支持套餐列表获取、订单创建及用户订单查询功能
This commit is contained in:
1 parent
d76bff2142
commit
d7dc1e2dbe
29 files changed
+3589
-37
No files matched your search
@@ -62,6 +62,19 @@ errors:
|
||||
"500012": "Authentication token expired"
|
||||
"500013": "Failed to generate token"
|
||||
|
||||
# Package order module (60xxxx)
|
||||
cu_package:
|
||||
"600001": "Package does not exist"
|
||||
"600002": "Package is discontinued"
|
||||
"600003": "Trial version purchase time limit"
|
||||
"600004": "Trial version does not support bulk purchase"
|
||||
"600005": "Trial version already purchased this month"
|
||||
|
||||
# Order module (61xxxx)
|
||||
cu_order:
|
||||
"601001": "Order does not exist"
|
||||
"601005": "License quantity exceeds limit"
|
||||
|
||||
# System general (90xxxx)
|
||||
system:
|
||||
"900001": "Invalid request parameters"
|
||||
|
||||
@@ -62,6 +62,19 @@ errors:
|
||||
"500012": "認証トークンが期限切れです"
|
||||
"500013": "トークン生成に失敗しました"
|
||||
|
||||
# セット料金モジュール (60xxxx)
|
||||
cu_package:
|
||||
"600001": "セット料金が存在しません"
|
||||
"600002": "セット料金は販売終了しました"
|
||||
"600003": "試用版購入時間制限"
|
||||
"600004": "試用版は一括購入をサポートしていません"
|
||||
"600005": "今月試用版を購入済み"
|
||||
|
||||
# 注文モジュール (61xxxx)
|
||||
cu_order:
|
||||
"601001": "注文が存在しません"
|
||||
"601005": "ライセンス数量が制限を超えています"
|
||||
|
||||
# システム共通 (90xxxx)
|
||||
system:
|
||||
"900001": "無効なリクエストパラメータ"
|
||||
|
||||
@@ -62,6 +62,19 @@ errors:
|
||||
"500012": "认证令牌已过期"
|
||||
"500013": "令牌生成失败"
|
||||
|
||||
# 套餐订单模块 (60xxxx)
|
||||
cu_package:
|
||||
"600001": "套餐不存在"
|
||||
"600002": "套餐已下架"
|
||||
"600003": "试用版购买时间限制"
|
||||
"600004": "试用版不支持批量购买"
|
||||
"600005": "当月已购买试用版"
|
||||
|
||||
# 订单模块 (61xxxx)
|
||||
cu_order:
|
||||
"601001": "订单不存在"
|
||||
"601005": "许可数量超出限制"
|
||||
|
||||
# 系统通用 (90xxxx)
|
||||
system:
|
||||
"900001": "请求参数无效"
|
||||
|
||||
@@ -119,15 +119,32 @@ default_error: "Unknown error"
|
||||
|
||||
- **错误消息**: 使用业务错误码,通过 `i18n.NewI18nErrorResponse()` 获取本地化错误信息
|
||||
- **成功消息**: 统一使用 `i18n.GetErrorMessage("000000", lang)` 获取本地化的"成功"消息
|
||||
- **自定义消息**: 支持通过第三个参数传入自定义错误消息,优先级高于本地化消息
|
||||
|
||||
### 2. 响应格式统一
|
||||
|
||||
```go
|
||||
// 错误响应
|
||||
// 错误响应 - 使用本地化消息
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("200001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message, // 显示本地化的错误消息
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
|
||||
// 错误响应 - 使用自定义消息(显示具体错误信息)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message, // 显示具体的错误信息,如"RSA私钥文件不存在..."
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
|
||||
// 错误响应 - 结合本地化消息和具体错误
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message + ": " + err.Error(),
|
||||
Message: message + ": " + err.Error(), // 本地化消息 + 具体错误信息
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
# 产品套餐体系设计
|
||||
|
||||
## 需求概述
|
||||
|
||||
构建套餐购买和授权管理系统,支持客户自主选择套餐、生成授权码的业务流程。
|
||||
|
||||
**核心要求:**
|
||||
- 支持三种套餐类型:试用版、基础版、专业版
|
||||
- 实现阶梯定价折扣机制(基于许可数量,通过配置化JSON实现)
|
||||
- 自动生成授权码(一个订单对应一个授权码,激活次数由许可数量决定)
|
||||
- 支付流程暂时用伪代码实现
|
||||
- 保证授权码的安全性和唯一性
|
||||
|
||||
**重要概念说明:**
|
||||
- **许可数量**:等于设备数量,决定授权码可以激活多少次
|
||||
- **试用版**:1个授权码,许可数量为1,用户每月限购1个
|
||||
- **基础版/专业版**:1个授权码,许可数量等于购买数量,支持50-99、100-499、500+许可的阶梯折扣
|
||||
|
||||
## 业务流程
|
||||
|
||||
### 整体购买流程
|
||||
1. 用户登录客户用户系统
|
||||
2. 浏览套餐列表和价格信息
|
||||
3. 选择套餐类型和许可数量
|
||||
4. 系统计算折扣和总价
|
||||
5. 用户确认并支付(支付成功后直接创建订单)
|
||||
6. 系统生成授权码并返回给用户
|
||||
7. 支付失败或取消:不创建订单,用户可重新下单
|
||||
|
||||
### 简化流程说明
|
||||
- **试用版**:直接生成授权码,无需支付
|
||||
- **基础版/专业版**:使用伪代码模拟支付成功,然后生成授权码
|
||||
- **一个订单**:对应一个授权码,数量字段仅用于价格计算
|
||||
|
||||
## 套餐类型详细说明
|
||||
|
||||
| 套餐类型 | 授权期限 | 单价(元)/许可 | 许可数量 | 功能限制 | 适用场景 |
|
||||
|---------|---------|----------|---------|---------|---------|
|
||||
| 试用版 | 当天至本月25日 | 0 | 1 | 全部功能 | 新用户试用体验 |
|
||||
| 基础版 | 永久授权 | 300 | 按购买数量 | 部分功能限制 | 小型企业入门 |
|
||||
| 专业版 | 永久授权 | 2000 | 按购买数量 | 全部功能 | 中大型企业使用 |
|
||||
|
||||
### 功能限制说明
|
||||
|
||||
**试用版:**
|
||||
- 全部功能开放,无任何限制
|
||||
- 时效性:每月1-25日可购买,当月25日到期
|
||||
- 不可批量购买,数量固定为1
|
||||
|
||||
**基础版:**
|
||||
- 限制同时在线设备数量(具体数量待定)
|
||||
- 限制某些高级功能的使用
|
||||
- 支持批量购买,享受阶梯折扣
|
||||
|
||||
**专业版:**
|
||||
- 全部功能开放,无任何限制
|
||||
- 支持批量购买,企业级折扣
|
||||
- 包含优先技术支持服务
|
||||
|
||||
## 购买规则和限制
|
||||
|
||||
### 折扣规则
|
||||
- **默认价格**:单个许可不享受折扣
|
||||
- **9折优惠**:购买50-99个许可
|
||||
- **8折优惠**:购买100-499个许可
|
||||
- **7折优惠**:购买500个及以上许可
|
||||
|
||||
### 试用版特殊规则
|
||||
- **购买时间限制**:每月1-25日可购买,26-31日不可购买
|
||||
- **授权期限**:从购买当天到当月25日23:59:59
|
||||
- **许可数量**:固定为1(只能激活1次)
|
||||
- **用户限制**:每个用户每月只能购买1个试用版授权码
|
||||
- **重复购买**:次月可重新购买,不限制购买次数
|
||||
|
||||
### 基础版/专业版规则
|
||||
- **许可数量选择**:支持选择1-1000个许可数量
|
||||
- **激活次数限制**:授权码的激活次数等于购买的许可数量
|
||||
- **折扣计算**:基于许可总数量计算折扣,按单价折扣
|
||||
- **授权期限**:购买后永久有效,无到期时间限制
|
||||
|
||||
### 通用限制
|
||||
- **用户限制**:每个客户用户账号可独立购买,不受其他账号影响
|
||||
- **支付限制**:必须完成支付后才能生成授权码
|
||||
- **退款政策**:购买后不支持退款,建议谨慎选择
|
||||
|
||||
## 套餐配置设计
|
||||
|
||||
### 写死JSON配置结构
|
||||
|
||||
套餐信息通过代码中的JSON配置实现,便于后续修改:
|
||||
|
||||
```go
|
||||
// pkg/config/packages.go
|
||||
type PackageConfig struct {
|
||||
Packages []Package `json:"packages"`
|
||||
Discounts []DiscountRule `json:"discounts"`
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // trial, basic, professional
|
||||
Price float64 `json:"price"`
|
||||
DurationDays *int `json:"duration_days"` // null表示永久
|
||||
MaxDevices *int `json:"max_devices"` // null表示无限制
|
||||
Features map[string]interface{} `json:"features"`
|
||||
Status string `json:"status"` // active, disabled
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
type DiscountRule struct {
|
||||
MinQuantity int `json:"min_quantity"`
|
||||
MaxQuantity *int `json:"max_quantity"` // null表示无上限
|
||||
Rate float64 `json:"rate"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// 写死配置数据
|
||||
var DefaultPackageConfig = PackageConfig{
|
||||
Packages: []Package{
|
||||
{
|
||||
ID: "trial",
|
||||
Name: "试用版",
|
||||
Type: "trial",
|
||||
Price: 0, // 免费
|
||||
DurationDays: nil, // 动态计算到当月25日
|
||||
MaxDevices: nil, // 无设备限制
|
||||
Features: map[string]interface{}{"full_access": true},
|
||||
Status: "active",
|
||||
SortOrder: 1,
|
||||
},
|
||||
{
|
||||
ID: "basic",
|
||||
Name: "基础版",
|
||||
Type: "basic",
|
||||
Price: 300, // 每许可300元
|
||||
DurationDays: nil, // 永久
|
||||
MaxDevices: nil, // 激活次数由购买数量决定
|
||||
Features: map[string]interface{}{"basic_features": true},
|
||||
Status: "active",
|
||||
SortOrder: 2,
|
||||
},
|
||||
{
|
||||
ID: "professional",
|
||||
Name: "专业版",
|
||||
Type: "professional",
|
||||
Price: 2000, // 每许可2000元
|
||||
DurationDays: nil, // 永久
|
||||
MaxDevices: nil, // 激活次数由购买数量决定
|
||||
Features: map[string]interface{}{"full_access": true, "priority_support": true},
|
||||
Status: "active",
|
||||
SortOrder: 3,
|
||||
},
|
||||
},
|
||||
// 折扣规则:基于许可数量
|
||||
Discounts: []DiscountRule{
|
||||
{MinQuantity: 50, MaxQuantity: &[]int{99}[0], Rate: 0.9, Description: "50-99许可9折优惠"},
|
||||
{MinQuantity: 100, MaxQuantity: &[]int{499}[0], Rate: 0.8, Description: "100-499许可8折优惠"},
|
||||
{MinQuantity: 500, MaxQuantity: nil, Rate: 0.7, Description: "500+许可7折优惠"},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 数据模型设计
|
||||
|
||||
### 订单表 (orders)
|
||||
```sql
|
||||
CREATE TABLE orders (
|
||||
id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
order_no VARCHAR(50) NOT NULL COMMENT '订单号',
|
||||
customer_id VARCHAR(36) NOT NULL COMMENT '客户ID',
|
||||
cu_user_id VARCHAR(36) NOT NULL COMMENT '客户用户ID',
|
||||
package_id VARCHAR(50) NOT NULL COMMENT '套餐ID (对应配置中的ID)',
|
||||
package_name VARCHAR(100) NOT NULL COMMENT '套餐名称(快照)',
|
||||
license_count INT NOT NULL COMMENT '许可数量(可激活次数)',
|
||||
unit_price DECIMAL(10,2) NOT NULL COMMENT '单价(每许可价格,快照)',
|
||||
discount_rate DECIMAL(3,2) NOT NULL DEFAULT 1.0 COMMENT '折扣率',
|
||||
total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '订单状态: pending-待支付, paid-已支付, cancelled-已取消, expired-已过期',
|
||||
authorization_code VARCHAR(50) NULL COMMENT '生成的授权码',
|
||||
expired_at DATETIME(3) NULL COMMENT '订单过期时间',
|
||||
created_at DATETIME(3) NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL,
|
||||
deleted_at DATETIME(3) NULL,
|
||||
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id),
|
||||
FOREIGN KEY (cu_user_id) REFERENCES cu_users(id)
|
||||
);
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 一个订单对应一个授权码,`license_count`字段表示许可数量
|
||||
- `authorization_code`字段存储生成的授权码
|
||||
- 授权码创建时会设置许可数量限制
|
||||
|
||||
## API接口设计
|
||||
|
||||
### 1. 获取套餐列表
|
||||
```
|
||||
GET /api/cu/packages
|
||||
```
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"id": "trial",
|
||||
"name": "试用版",
|
||||
"type": "trial",
|
||||
"price": 0,
|
||||
"max_devices": 1,
|
||||
"description": "免费试用,有效期至本月25日"
|
||||
},
|
||||
{
|
||||
"id": "basic",
|
||||
"name": "基础版",
|
||||
"type": "basic",
|
||||
"price": 300,
|
||||
"max_devices": 1000,
|
||||
"description": "基础功能,支持批量许可购买"
|
||||
},
|
||||
{
|
||||
"id": "professional",
|
||||
"name": "专业版",
|
||||
"type": "professional",
|
||||
"price": 2000,
|
||||
"max_devices": 1000,
|
||||
"description": "全部功能,支持批量许可购买"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 计算订单价格
|
||||
```
|
||||
POST /api/cu/orders/calculate
|
||||
```
|
||||
**请求:**
|
||||
```json
|
||||
{
|
||||
"package_id": "basic",
|
||||
"license_count": 100
|
||||
}
|
||||
```
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"unit_price": 300.00,
|
||||
"license_count": 100,
|
||||
"discount_rate": 0.8,
|
||||
"total_amount": 24000.00,
|
||||
"discount_description": "100-499许可8折优惠"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 创建订单
|
||||
```
|
||||
POST /api/cu/orders
|
||||
```
|
||||
**请求:**
|
||||
```json
|
||||
{
|
||||
"package_id": "basic",
|
||||
"license_count": 100
|
||||
}
|
||||
```
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"order_id": "order_id_123",
|
||||
"order_no": "ORD202401010001",
|
||||
"total_amount": 24000.00,
|
||||
"status": "paid",
|
||||
"authorization_code": "AC-240101-A1B2C3D4",
|
||||
"expired_at": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 获取订单详情
|
||||
```
|
||||
GET /api/cu/orders/{order_id}
|
||||
```
|
||||
返回订单状态和授权码信息。
|
||||
|
||||
### 5. 获取用户订单历史
|
||||
```
|
||||
GET /api/cu/orders
|
||||
```
|
||||
返回用户的订单历史记录。
|
||||
|
||||
## 前端交互需求
|
||||
|
||||
### 套餐选择页面
|
||||
- 展示三种套餐类型的详细信息
|
||||
- 支持数量选择(试用版除外)
|
||||
- 实时计算折扣和总价
|
||||
- 显示购买限制和规则说明
|
||||
|
||||
### 订单确认页面
|
||||
- 显示订单详情和价格明细
|
||||
- 提供支付方式选择
|
||||
- 显示订单过期时间提醒
|
||||
|
||||
### 支付结果页面
|
||||
- 显示支付状态
|
||||
- 支付成功后展示生成的授权码
|
||||
- 提供授权码下载/复制功能
|
||||
|
||||
### 订单历史页面
|
||||
- 显示用户的所有订单记录
|
||||
- 支持按状态筛选
|
||||
- 显示授权码详情和使用情况
|
||||
|
||||
## 支付集成 (暂时用伪代码实现)
|
||||
|
||||
### 支付流程说明
|
||||
由于支付功能暂时不实现,使用伪代码直接模拟支付成功:
|
||||
|
||||
```go
|
||||
// 伪代码:模拟支付流程
|
||||
func simulatePayment(order *Order) error {
|
||||
// TODO: 替换为真实的支付接口
|
||||
// 1. 调用第三方支付接口
|
||||
// 2. 返回支付链接或二维码
|
||||
// 3. 等待支付回调
|
||||
|
||||
// 暂时直接标记为支付成功
|
||||
order.Status = "paid"
|
||||
order.AuthorizationCode = generateAuthorizationCode()
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 支付状态流转
|
||||
1. **所有订单**:创建时即为已支付状态
|
||||
2. **试用版**:免费,无需支付流程
|
||||
3. **基础版/专业版**:支付成功后创建订单,失败则不创建
|
||||
4. **无中间状态**:要么创建成功(已支付),要么失败不创建
|
||||
|
||||
### 未来扩展点
|
||||
- 集成支付宝/微信支付SDK
|
||||
- 实现支付回调接口
|
||||
- 添加支付超时和失败处理
|
||||
- 实现退款功能
|
||||
|
||||
## 授权码生成逻辑
|
||||
|
||||
### 授权码生成流程
|
||||
1. 根据套餐类型从配置中获取授权参数
|
||||
2. 生成唯一的授权码(格式:AC-年月日-8位随机数)
|
||||
3. 设置授权期限和许可数量限制
|
||||
4. 创建订单记录和授权码记录
|
||||
|
||||
### 授权码格式规范
|
||||
- **前缀**:AC (Authorization Code)
|
||||
- **日期**:YYMMDD格式(如240101)
|
||||
- **随机数**:8位数字字母组合(排除容易混淆的字符如0、O、I、1)
|
||||
- **示例**:AC-240101-A1B2C3D4
|
||||
|
||||
### 试用版特殊处理
|
||||
- **购买限制**:每个用户每月只能有一个试用版授权码
|
||||
- **时间限制**:每月1-25日可购买,26-31日不可购买
|
||||
- **到期时间**:购买当月25日23:59:59
|
||||
- **许可数量**:固定为1(只能激活1次)
|
||||
- **重复购买**:次月可重新购买,不限制购买次数
|
||||
|
||||
|
||||
|
||||
**说明:** 由于一个订单对应一个授权码,不再需要批量生成逻辑。
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 新增表
|
||||
1. **orders** - 订单表
|
||||
|
||||
### 索引设计
|
||||
```sql
|
||||
-- orders表索引
|
||||
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
|
||||
CREATE INDEX idx_orders_cu_user_id ON orders(cu_user_id);
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
CREATE INDEX idx_orders_created_at ON orders(created_at);
|
||||
CREATE INDEX idx_orders_package_id ON orders(package_id);
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 业务错误码 (60xxxx)
|
||||
```yaml
|
||||
# 套餐相关错误
|
||||
cu_package:
|
||||
"600001": "套餐不存在"
|
||||
"600002": "套餐已下架"
|
||||
"600003": "试用版购买时间限制"
|
||||
"600004": "试用版不支持批量购买"
|
||||
"600005": "当月已购买试用版"
|
||||
|
||||
# 订单相关错误
|
||||
cu_order:
|
||||
"601001": "订单不存在"
|
||||
"601005": "许可数量超出限制"
|
||||
|
||||
# 支付相关错误
|
||||
cu_payment:
|
||||
"602001": "支付失败"
|
||||
"602002": "支付金额不匹配"
|
||||
"602003": "重复支付"
|
||||
```
|
||||
|
||||
## 多语言支持
|
||||
|
||||
### 套餐名称国际化
|
||||
```yaml
|
||||
packages:
|
||||
trial: "试用版"
|
||||
basic: "基础版"
|
||||
professional: "专业版"
|
||||
```
|
||||
|
||||
### 折扣说明国际化
|
||||
```yaml
|
||||
discounts:
|
||||
no_discount: "不享受折扣"
|
||||
discount_9: "9折优惠 (50-99个)"
|
||||
discount_8: "8折优惠 (100-499个)"
|
||||
discount_7: "7折优惠 (500个以上)"
|
||||
```
|
||||
|
||||
## 技术实现要点
|
||||
|
||||
### 订单号生成
|
||||
- 格式:ORD + 年月日 + 6位序列号
|
||||
- 保证全局唯一性
|
||||
- 支持高并发生成
|
||||
|
||||
### 折扣计算逻辑
|
||||
基于许可数量的配置化折扣规则:
|
||||
|
||||
```go
|
||||
func calculateDiscount(config PackageConfig, licenseCount int) (rate float64, description string) {
|
||||
for _, rule := range config.Discounts {
|
||||
if licenseCount >= rule.MinQuantity {
|
||||
if rule.MaxQuantity == nil || licenseCount <= *rule.MaxQuantity {
|
||||
return rule.Rate, rule.Description
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1.0, "不享受折扣"
|
||||
}
|
||||
```
|
||||
|
||||
### 试用版时间限制检查
|
||||
```go
|
||||
func canPurchaseTrial() bool {
|
||||
now := time.Now()
|
||||
// 每月1-25日可购买
|
||||
return now.Day >= 1 && now.Day <= 25
|
||||
}
|
||||
|
||||
func getTrialExpiryTime() time.Time {
|
||||
now := time.Now()
|
||||
// 当月25日23:59:59
|
||||
return time.Date(now.Year(), now.Month(), 25, 23, 59, 59, 0, now.Location())
|
||||
}
|
||||
```
|
||||
|
||||
### 授权码生成
|
||||
```go
|
||||
func generateAuthorizationCode() string {
|
||||
now := time.Now()
|
||||
dateStr := now.Format("060102") // YYMMDD格式
|
||||
|
||||
// 生成8位随机字符串(数字+字母,排除易混淆字符)
|
||||
const charset = "23456789ABCDEFGHJKMNPQRSTUVWXYZ"
|
||||
code := make([]byte, 8)
|
||||
for i := range code {
|
||||
code[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("AC-%s-%s", dateStr, string(code))
|
||||
}
|
||||
```
|
||||
|
||||
### 订单号生成
|
||||
```go
|
||||
func generateOrderNo() string {
|
||||
now := time.Now()
|
||||
dateStr := now.Format("20060102") // YYYYMMDD格式
|
||||
|
||||
// 使用原子计数器生成6位序列号
|
||||
sequence := atomic.AddUint64(&orderSequence, 1) % 1000000
|
||||
|
||||
return fmt.Sprintf("ORD%s%06d", dateStr, sequence)
|
||||
}
|
||||
```
|
||||
|
||||
## 验收标准
|
||||
|
||||
### 功能验收
|
||||
- [x] 套餐列表接口正确返回三种套餐信息(公开接口,无需认证)
|
||||
- [x] 价格计算接口正确处理折扣逻辑
|
||||
- [x] 订单创建接口完整实现业务流程
|
||||
- [x] 获取订单详情接口支持权限验证
|
||||
- [x] 获取用户订单历史接口支持分页
|
||||
- [ ] 试用版购买时间限制生效
|
||||
- [ ] 试用版每月限购一个的检查逻辑正确
|
||||
- [ ] 授权码生成正确(包含激活次数限制)
|
||||
- [x] 订单创建即为已支付状态,无需状态更新
|
||||
- [ ] 授权码格式规范统一
|
||||
|
||||
### 性能验收
|
||||
- [ ] 订单创建响应时间 < 500ms
|
||||
- [ ] 授权码生成 < 100ms
|
||||
- [ ] 高并发订单处理稳定
|
||||
- [ ] 数据库查询性能良好
|
||||
|
||||
### 安全验收
|
||||
- [ ] 订单金额验证严格
|
||||
- [ ] 防止重复生成授权码
|
||||
- [ ] 授权码唯一性保证
|
||||
- [ ] 敏感信息处理安全
|
||||
|
||||
### 业务验收
|
||||
- [ ] 折扣规则计算准确(基于许可数量和配置JSON)
|
||||
- [ ] 试用版期限设置正确(当月25日)
|
||||
- [ ] 试用版用户每月限购一个
|
||||
- [ ] 永久版无到期时间限制
|
||||
- [ ] 许可数量设置正确(试用版1,其他版等于购买数量)
|
||||
- [ ] 一个订单对应一个授权码
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
# 授权码生成规范变更
|
||||
|
||||
## 📋 需求背景
|
||||
|
||||
### 现状分析
|
||||
- 当前授权码格式:`AC-260101-PQRSTUVW` (示例格式)
|
||||
- 用户购买授权码后,需要在软件端激活使用
|
||||
- 软件使用环境可能不联网,无法实时验证授权码有效性
|
||||
- 当前授权码仅包含基本标识信息,缺少配置详情
|
||||
|
||||
### 业务痛点
|
||||
1. **离线激活困难**:无网络环境无法验证授权码
|
||||
2. **配置信息缺失**:授权码不包含功能配置、使用限制等详细信息
|
||||
3. **用户体验不佳**:激活流程依赖网络连接
|
||||
4. **格式不够友好**:授权码可读性差,不易识别
|
||||
|
||||
## 🎯 需求目标
|
||||
|
||||
### 核心目标
|
||||
实现**离线激活授权码**,支持无网络环境下的软件激活和配置获取。
|
||||
|
||||
### 功能目标
|
||||
1. **完整配置嵌入**:授权码包含所有必要配置信息
|
||||
2. **加密签名保护**:通过数字签名确保授权码安全性
|
||||
3. **离线验证支持**:软件端可独立验证授权码有效性
|
||||
4. **友好格式设计**:提高授权码的可读性和识别性
|
||||
|
||||
## 🏗️ 技术方案设计
|
||||
|
||||
### 1. 授权码结构设计
|
||||
|
||||
#### 自包含式授权码
|
||||
授权码本身包含完整的配置信息,通过编码和加密实现:
|
||||
|
||||
**授权码格式**:
|
||||
```
|
||||
LIC-{压缩编码的配置数据}-{校验码}
|
||||
```
|
||||
|
||||
**生成步骤**:
|
||||
1. 构建核心配置JSON
|
||||
2. 进行数据压缩和编码
|
||||
3. 添加RSA签名
|
||||
4. 生成校验码
|
||||
5. 格式化为用户友好的字符串
|
||||
|
||||
### 2. 数据编码方案
|
||||
|
||||
#### 核心配置数据
|
||||
```json
|
||||
{
|
||||
"ver": "1.0",
|
||||
"exp": "2026-12-31T23:59:59Z",
|
||||
"features": {
|
||||
"max_users": 100,
|
||||
"storage": "100GB"
|
||||
},
|
||||
"limits": {
|
||||
"type": "standard"
|
||||
},
|
||||
"params": {
|
||||
"company": "示例公司"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 编码过程
|
||||
1. **JSON序列化**:将配置转换为紧凑JSON格式
|
||||
2. **数据压缩**:使用高效压缩算法减小体积
|
||||
3. **Base64编码**:转换为安全字符集
|
||||
4. **RSA签名**:对编码数据进行签名
|
||||
5. **格式封装**:添加前缀和校验码
|
||||
|
||||
#### 解码过程
|
||||
1. **格式解析**:提取编码数据部分
|
||||
2. **签名验证**:验证RSA签名有效性
|
||||
3. **数据解码**:Base64解码并解压缩
|
||||
4. **配置提取**:解析JSON获取配置信息
|
||||
|
||||
### 3. 加密与签名
|
||||
|
||||
#### 密钥管理
|
||||
- **私钥**: 与生成许可证使用相同的RSA私钥
|
||||
- **公钥**: 软件客户端内置对应的RSA公钥
|
||||
- **自动生成**: 如果密钥文件不存在,系统会自动生成RSA密钥对
|
||||
- **数据签名**: 对编码后的配置数据进行RSA签名
|
||||
|
||||
#### 签名算法
|
||||
- **算法**: RS256 (RSA + SHA256)
|
||||
- **签名位置**: 签名数据嵌入到授权码中
|
||||
- **验证方式**: 软件端提取并验证签名确保数据完整性
|
||||
|
||||
### 4. 激活流程设计
|
||||
|
||||
#### 完全离线激活流程
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant S as 软件客户端
|
||||
|
||||
U->>S: 输入授权码
|
||||
S->>S: 解析授权码格式
|
||||
S->>S: 验证RSA签名
|
||||
S->>S: 解码配置数据
|
||||
S->>S: 验证到期时间
|
||||
S->>S: 激活软件功能
|
||||
|
||||
Note over S: 完全离线,授权码自包含所有信息
|
||||
```
|
||||
|
||||
## 🔒 安全性考虑
|
||||
|
||||
### 加密保护
|
||||
- **RSA签名**: 使用与许可证相同的私钥进行签名
|
||||
- **数据完整性**: 防止配置信息被篡改
|
||||
- **到期验证**: 确保授权码在有效期内使用
|
||||
|
||||
### 验证机制
|
||||
- **格式校验**: 验证授权码格式规范
|
||||
- **签名验证**: 强制验证RSA签名有效性
|
||||
- **时间检查**: 验证到期时间未过期
|
||||
|
||||
## 📊 验收标准
|
||||
|
||||
### 功能验收
|
||||
- [ ] 授权码自包含配置信息(到期时间、功能配置、使用限制、自定义参数)
|
||||
- [ ] 授权码通过RSA签名确保数据完整性
|
||||
- [ ] 软件端可完全离线解析和验证授权码
|
||||
- [ ] 激活流程无需网络连接
|
||||
|
||||
### 安全性验收
|
||||
- [ ] RSA签名验证通过率 100%
|
||||
- [ ] 配置数据防篡改保护
|
||||
- [ ] 到期时间验证准确
|
||||
- [ ] 编码/解码过程可靠
|
||||
|
||||
## 🔧 简化实施计划
|
||||
|
||||
### 📋 当前实现分析
|
||||
|
||||
#### 现有架构保持不变
|
||||
- **数据库结构**: 完全保持现有设计
|
||||
- **业务逻辑**: 不改变现有流程
|
||||
- **API接口**: 现有接口继续可用
|
||||
|
||||
#### 核心变化
|
||||
- **授权码生成规则**: 从简单字符串改为自包含配置的编码字符串
|
||||
- **新增工具包**: `pkg/license/codec.go` 提供编解码功能
|
||||
- **字段长度调整**: `code` 字段长度从 100 调整到 1000 以容纳编码数据
|
||||
|
||||
### 🎯 简化的修改计划
|
||||
|
||||
#### 第一阶段:开发核心算法包
|
||||
1. **扩展 `pkg/utils/license_codec.go`**
|
||||
- `EncodeLicenseData()`: 配置数据编码为授权码
|
||||
- `DecodeLicenseData()`: 从授权码解析配置数据
|
||||
- `SignLicenseData()`: RSA签名生成
|
||||
- `VerifyLicenseSignature()`: RSA签名验证
|
||||
|
||||
#### 第二阶段:修改授权码生成
|
||||
1. **更新 `generateAuthorizationCode()` 方法**
|
||||
- 集成配置数据编码逻辑
|
||||
- 生成自包含的授权码字符串
|
||||
- 保持相同的调用接口
|
||||
|
||||
2. **调整数据库字段长度**
|
||||
- `code VARCHAR(100)` → `code VARCHAR(1000)`
|
||||
- 确保向后兼容现有短授权码
|
||||
|
||||
#### 第三阶段:完成授权码生成规则
|
||||
1. **集成到现有流程**
|
||||
- 修改授权码生成服务
|
||||
- 保持现有API接口不变
|
||||
- 授权码自包含配置信息
|
||||
|
||||
### 📊 验收标准
|
||||
|
||||
#### 功能验收
|
||||
- [ ] 授权码包含完整配置信息(到期时间、功能配置、使用限制、自定义参数)
|
||||
- [ ] 授权码通过RSA签名确保数据完整性
|
||||
- [ ] 授权码可被软件端离线解析和验证
|
||||
- [ ] 保持与现有系统的完全兼容性
|
||||
|
||||
#### 性能验收
|
||||
- [ ] 授权码生成时间 < 200ms (增加约50ms用于编码)
|
||||
- [ ] 离线验证时间 < 100ms
|
||||
|
||||
#### 安全性验收
|
||||
- [ ] RSA签名验证功能正常
|
||||
- [ ] 配置数据防篡改保护
|
||||
|
||||
### 🔄 兼容性策略
|
||||
|
||||
#### 渐进式启用
|
||||
- 默认保持原有生成方式(可配置开关)
|
||||
- 新授权码采用自包含格式
|
||||
- 现有授权码继续有效
|
||||
|
||||
### 📝 具体代码修改点
|
||||
|
||||
#### 1. 新增文件:`pkg/utils/license_codec.go`
|
||||
```go
|
||||
package utils
|
||||
|
||||
// 核心编解码和签名验证功能
|
||||
func EncodeLicenseData(config interface{}) (string, error)
|
||||
func DecodeLicenseData(code string) (interface{}, error)
|
||||
func SignLicenseData(data []byte) (string, error)
|
||||
func VerifyLicenseSignature(data []byte, signature string) error
|
||||
```
|
||||
|
||||
#### 2. 修改文件:`authorization_code_service.go`
|
||||
- 修改 `CreateAuthorizationCode()` 方法支持预生成授权码
|
||||
- 添加 `PreGeneratedCode` 参数到请求结构
|
||||
- 保持向后兼容性
|
||||
|
||||
#### 3. 修改文件:`cu_order_service.go`
|
||||
- 修改订单创建流程生成自包含授权码
|
||||
- 使用 `utils.EncodeLicenseData()` 生成授权码
|
||||
- 传递预生成的授权码给授权码服务
|
||||
|
||||
#### 4. 修改文件:`003_create_authorization_codes_table.sql`
|
||||
- 调整 `code VARCHAR(100)` → `code VARCHAR(1000)`
|
||||
|
||||
|
||||
## 📝 附录
|
||||
|
||||
### 配置参数说明
|
||||
- **end_date**: 到期时间
|
||||
- **feature_config**: 功能特性配置
|
||||
- **usage_limits**: 使用限制配置
|
||||
- **custom_parameters**: 自定义业务参数
|
||||
@@ -172,6 +172,371 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/orders": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取当前用户的订单列表,支持分页",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "获取用户订单列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "页码",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "每页数量",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/models.CuOrderListResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "创建新的产品套餐订单",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "创建订单",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "订单创建信息",
|
||||
"name": "order",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.CuOrderCreateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorization_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"order_no": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_amount": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数无效",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "套餐不存在",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "当月已购买试用版",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/orders/calculate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据套餐和许可数量计算价格和折扣",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "计算订单价格",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "价格计算请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"license_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"package_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discount_description": {
|
||||
"type": "string"
|
||||
},
|
||||
"discount_rate": {
|
||||
"type": "number"
|
||||
},
|
||||
"license_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"unit_price": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数无效",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "套餐不存在",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/orders/{order_id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据订单ID获取订单详细信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "获取订单详情",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "订单ID",
|
||||
"name": "order_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/models.CuOrderResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "权限不足",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "订单不存在",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/packages": {
|
||||
"get": {
|
||||
"description": "获取所有可用的产品套餐信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "获取套餐列表",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_devices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/profile": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3454,6 +3819,93 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CuOrderCreateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"license_count",
|
||||
"package_id"
|
||||
],
|
||||
"properties": {
|
||||
"license_count": {
|
||||
"type": "integer",
|
||||
"maximum": 1000,
|
||||
"minimum": 1
|
||||
},
|
||||
"package_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CuOrderListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"orders": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.CuOrderResponse"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"page_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CuOrderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorization_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"cu_user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"customer_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"discount_rate": {
|
||||
"type": "number"
|
||||
},
|
||||
"expired_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"license_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"order_no": {
|
||||
"type": "string"
|
||||
},
|
||||
"package_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"package_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"unit_price": {
|
||||
"type": "number"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CuUserChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -3499,6 +3951,12 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"maxLength": 20,
|
||||
"minLength": 7
|
||||
},
|
||||
"phone_country_code": {
|
||||
"description": "可选,默认+86",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,371 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/orders": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取当前用户的订单列表,支持分页",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "获取用户订单列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "页码",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "每页数量",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/models.CuOrderListResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "创建新的产品套餐订单",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "创建订单",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "订单创建信息",
|
||||
"name": "order",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.CuOrderCreateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorization_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"order_no": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_amount": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数无效",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "套餐不存在",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "当月已购买试用版",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/orders/calculate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据套餐和许可数量计算价格和折扣",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "计算订单价格",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "价格计算请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"license_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"package_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discount_description": {
|
||||
"type": "string"
|
||||
},
|
||||
"discount_rate": {
|
||||
"type": "number"
|
||||
},
|
||||
"license_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"unit_price": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数无效",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "套餐不存在",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/orders/{order_id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据订单ID获取订单详细信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "获取订单详情",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "订单ID",
|
||||
"name": "order_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/models.CuOrderResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "权限不足",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "订单不存在",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/packages": {
|
||||
"get": {
|
||||
"description": "获取所有可用的产品套餐信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"客户订单管理"
|
||||
],
|
||||
"summary": "获取套餐列表",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_devices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/cu/profile": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3450,6 +3815,93 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CuOrderCreateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"license_count",
|
||||
"package_id"
|
||||
],
|
||||
"properties": {
|
||||
"license_count": {
|
||||
"type": "integer",
|
||||
"maximum": 1000,
|
||||
"minimum": 1
|
||||
},
|
||||
"package_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CuOrderListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"orders": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.CuOrderResponse"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"page_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CuOrderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorization_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"cu_user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"customer_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"discount_rate": {
|
||||
"type": "number"
|
||||
},
|
||||
"expired_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"license_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"order_no": {
|
||||
"type": "string"
|
||||
},
|
||||
"package_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"package_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"unit_price": {
|
||||
"type": "number"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CuUserChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -3495,6 +3947,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"maxLength": 20,
|
||||
"minLength": 7
|
||||
},
|
||||
"phone_country_code": {
|
||||
"description": "可选,默认+86",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,6 +408,64 @@ definitions:
|
||||
description: 总许可证数量
|
||||
type: integer
|
||||
type: object
|
||||
models.CuOrderCreateRequest:
|
||||
properties:
|
||||
license_count:
|
||||
maximum: 1000
|
||||
minimum: 1
|
||||
type: integer
|
||||
package_id:
|
||||
type: string
|
||||
required:
|
||||
- license_count
|
||||
- package_id
|
||||
type: object
|
||||
models.CuOrderListResponse:
|
||||
properties:
|
||||
orders:
|
||||
items:
|
||||
$ref: '#/definitions/models.CuOrderResponse'
|
||||
type: array
|
||||
page:
|
||||
type: integer
|
||||
page_size:
|
||||
type: integer
|
||||
total_count:
|
||||
type: integer
|
||||
type: object
|
||||
models.CuOrderResponse:
|
||||
properties:
|
||||
authorization_code:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
cu_user_id:
|
||||
type: string
|
||||
customer_id:
|
||||
type: string
|
||||
discount_rate:
|
||||
type: number
|
||||
expired_at:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
license_count:
|
||||
type: integer
|
||||
order_no:
|
||||
type: string
|
||||
package_id:
|
||||
type: string
|
||||
package_name:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
total_amount:
|
||||
type: number
|
||||
unit_price:
|
||||
type: number
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
models.CuUserChangePasswordRequest:
|
||||
properties:
|
||||
new_password:
|
||||
@@ -437,6 +495,11 @@ definitions:
|
||||
password:
|
||||
type: string
|
||||
phone:
|
||||
maxLength: 20
|
||||
minLength: 7
|
||||
type: string
|
||||
phone_country_code:
|
||||
description: 可选,默认+86
|
||||
type: string
|
||||
required:
|
||||
- password
|
||||
@@ -1385,6 +1448,226 @@ paths:
|
||||
summary: 客户用户登录
|
||||
tags:
|
||||
- 客户用户管理
|
||||
/api/cu/orders:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取当前用户的订单列表,支持分页
|
||||
parameters:
|
||||
- default: 1
|
||||
description: 页码
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- default: 10
|
||||
description: 每页数量
|
||||
in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.CuOrderListResponse'
|
||||
type: object
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取用户订单列表
|
||||
tags:
|
||||
- 客户订单管理
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 创建新的产品套餐订单
|
||||
parameters:
|
||||
- description: 订单创建信息
|
||||
in: body
|
||||
name: order
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.CuOrderCreateRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
properties:
|
||||
authorization_code:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
order_no:
|
||||
type: string
|
||||
total_amount:
|
||||
type: number
|
||||
type: object
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"404":
|
||||
description: 套餐不存在
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"409":
|
||||
description: 当月已购买试用版
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 创建订单
|
||||
tags:
|
||||
- 客户订单管理
|
||||
/api/cu/orders/{order_id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据订单ID获取订单详细信息
|
||||
parameters:
|
||||
- description: 订单ID
|
||||
in: path
|
||||
name: order_id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.CuOrderResponse'
|
||||
type: object
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"403":
|
||||
description: 权限不足
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"404":
|
||||
description: 订单不存在
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取订单详情
|
||||
tags:
|
||||
- 客户订单管理
|
||||
/api/cu/orders/calculate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据套餐和许可数量计算价格和折扣
|
||||
parameters:
|
||||
- description: 价格计算请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
properties:
|
||||
license_count:
|
||||
type: integer
|
||||
package_id:
|
||||
type: string
|
||||
type: object
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
properties:
|
||||
discount_description:
|
||||
type: string
|
||||
discount_rate:
|
||||
type: number
|
||||
license_count:
|
||||
type: integer
|
||||
total_amount:
|
||||
type: number
|
||||
unit_price:
|
||||
type: number
|
||||
type: object
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"404":
|
||||
description: 套餐不存在
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 计算订单价格
|
||||
tags:
|
||||
- 客户订单管理
|
||||
/api/cu/packages:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取所有可用的产品套餐信息
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
max_devices:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
summary: 获取套餐列表
|
||||
tags:
|
||||
- 客户订单管理
|
||||
/api/cu/profile:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
+4
-1
@@ -6,8 +6,10 @@ require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
@@ -26,6 +28,7 @@ require (
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@@ -54,10 +57,10 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
|
||||
@@ -122,6 +122,8 @@ github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/i
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"license-manager/internal/api/middleware"
|
||||
"license-manager/internal/models"
|
||||
@@ -49,7 +50,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
|
||||
// 获取客户端IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
|
||||
data, err := h.authService.Login(c.Request.Context(), &req, clientIP)
|
||||
if err != nil {
|
||||
errorCode := "100003" // 登录失败
|
||||
@@ -154,7 +155,7 @@ func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
|
||||
// getCurrentTimestamp 获取当前时间戳
|
||||
func getCurrentTimestamp() string {
|
||||
return "2024-07-30T12:00:00Z" // 简化实现,实际应该使用time.Now().Format(time.RFC3339)
|
||||
return time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// extractTokenFromHeader 从请求头提取Token
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"license-manager/internal/api/middleware"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/service"
|
||||
"license-manager/pkg/i18n"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CuOrderHandler struct {
|
||||
cuOrderService service.CuOrderService
|
||||
}
|
||||
|
||||
func NewCuOrderHandler(cuOrderService service.CuOrderService) *CuOrderHandler {
|
||||
return &CuOrderHandler{
|
||||
cuOrderService: cuOrderService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPackages 获取套餐列表
|
||||
// @Summary 获取套餐列表
|
||||
// @Description 获取所有可用的产品套餐信息
|
||||
// @Tags 客户订单管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.APIResponse{data=[]object{id=string,name=string,type=string,price=number,max_devices=integer}} "成功"
|
||||
// @Router /api/cu/packages [get]
|
||||
func (h *CuOrderHandler) GetPackages(c *gin.Context) {
|
||||
lang := middleware.GetLanguage(c)
|
||||
|
||||
// 获取套餐列表
|
||||
packages := h.getPackageList()
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: i18n.GetI18nErrorMessage("000000", lang),
|
||||
Data: packages,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
}
|
||||
|
||||
// getPackageList 获取套餐列表(暂时写死数据)
|
||||
func (h *CuOrderHandler) getPackageList() []map[string]interface{} {
|
||||
return []map[string]interface{}{
|
||||
{
|
||||
"id": "trial",
|
||||
"name": "试用版",
|
||||
"type": "trial",
|
||||
"price": 0,
|
||||
"max_devices": 1,
|
||||
"description": "免费试用,有效期至本月25日",
|
||||
},
|
||||
{
|
||||
"id": "basic",
|
||||
"name": "基础版",
|
||||
"type": "basic",
|
||||
"price": 300,
|
||||
"max_devices": 1000,
|
||||
"description": "基础功能,支持批量许可购买",
|
||||
},
|
||||
{
|
||||
"id": "professional",
|
||||
"name": "专业版",
|
||||
"type": "professional",
|
||||
"price": 2000,
|
||||
"max_devices": 1000,
|
||||
"description": "全部功能,支持批量许可购买",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CalculatePrice 计算订单价格
|
||||
// @Summary 计算订单价格
|
||||
// @Description 根据套餐和许可数量计算价格和折扣
|
||||
// @Tags 客户订单管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body object{package_id=string,license_count=integer} true "价格计算请求"
|
||||
// @Success 200 {object} models.APIResponse{data=object{unit_price=number,license_count=integer,discount_rate=number,total_amount=number,discount_description=string}} "成功"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 404 {object} models.ErrorResponse "套餐不存在"
|
||||
// @Router /api/cu/orders/calculate [post]
|
||||
func (h *CuOrderHandler) CalculatePrice(c *gin.Context) {
|
||||
var req struct {
|
||||
PackageID string `json:"package_id" binding:"required"`
|
||||
LicenseCount int `json:"license_count" binding:"required,min=1,max=1000"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message + ": " + err.Error(),
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户ID(用于后续业务逻辑扩展)
|
||||
_, exists := c.Get("cu_user_id")
|
||||
if !exists {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("100004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 计算价格
|
||||
result, err := h.cuOrderService.CalculatePrice(c.Request.Context(), req.PackageID, req.LicenseCount)
|
||||
if err != nil {
|
||||
c.JSON(err.(*i18n.I18nError).HttpCode, models.ErrorResponse{
|
||||
Code: err.(*i18n.I18nError).Code,
|
||||
Message: err.(*i18n.I18nError).Message, // 直接使用错误对象中的消息
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMsg := i18n.GetI18nErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMsg,
|
||||
Data: result,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
}
|
||||
|
||||
// CreateOrder 创建订单
|
||||
// @Summary 创建订单
|
||||
// @Description 创建新的产品套餐订单
|
||||
// @Tags 客户订单管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param order body models.CuOrderCreateRequest true "订单创建信息"
|
||||
// @Success 200 {object} models.APIResponse{data=object{id=string,order_no=string,total_amount=number,authorization_code=string}} "成功"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 404 {object} models.ErrorResponse "套餐不存在"
|
||||
// @Failure 409 {object} models.ErrorResponse "当月已购买试用版"
|
||||
// @Router /api/cu/orders [post]
|
||||
func (h *CuOrderHandler) CreateOrder(c *gin.Context) {
|
||||
var req models.CuOrderCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message + ": " + err.Error(),
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户ID和客户ID
|
||||
cuUserID, exists := c.Get("cu_user_id")
|
||||
if !exists {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("100004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
customerID, exists := c.Get("cu_customer_id")
|
||||
if !exists {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("100004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
order, err := h.cuOrderService.CreateOrder(c.Request.Context(), cuUserID.(string), customerID.(string), &req)
|
||||
if err != nil {
|
||||
c.JSON(err.(*i18n.I18nError).HttpCode, models.ErrorResponse{
|
||||
Code: err.(*i18n.I18nError).Code,
|
||||
Message: err.(*i18n.I18nError).Message, // 直接使用错误对象中的消息
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMsg := i18n.GetI18nErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMsg,
|
||||
Data: gin.H{
|
||||
"id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"total_amount": order.TotalAmount,
|
||||
"authorization_code": order.AuthorizationCode,
|
||||
},
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetOrder 获取订单详情
|
||||
// @Summary 获取订单详情
|
||||
// @Description 根据订单ID获取订单详细信息
|
||||
// @Tags 客户订单管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param order_id path string true "订单ID"
|
||||
// @Success 200 {object} models.APIResponse{data=models.CuOrderResponse} "成功"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 403 {object} models.ErrorResponse "权限不足"
|
||||
// @Failure 404 {object} models.ErrorResponse "订单不存在"
|
||||
// @Router /api/cu/orders/{order_id} [get]
|
||||
func (h *CuOrderHandler) GetOrder(c *gin.Context) {
|
||||
orderID := c.Param("order_id")
|
||||
if orderID == "" {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
cuUserID, exists := c.Get("cu_user_id")
|
||||
if !exists {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("100004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
order, err := h.cuOrderService.GetOrder(c.Request.Context(), orderID, cuUserID.(string))
|
||||
if err != nil {
|
||||
c.JSON(err.(*i18n.I18nError).HttpCode, models.ErrorResponse{
|
||||
Code: err.(*i18n.I18nError).Code,
|
||||
Message: err.(*i18n.I18nError).Message, // 直接使用错误对象中的消息
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMsg := i18n.GetI18nErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMsg,
|
||||
Data: order.ToResponse(),
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserOrders 获取用户订单列表
|
||||
// @Summary 获取用户订单列表
|
||||
// @Description 获取当前用户的订单列表,支持分页
|
||||
// @Tags 客户订单管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param page query integer false "页码" default(1)
|
||||
// @Param page_size query integer false "每页数量" default(10)
|
||||
// @Success 200 {object} models.APIResponse{data=models.CuOrderListResponse} "成功"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Router /api/cu/orders [get]
|
||||
func (h *CuOrderHandler) GetUserOrders(c *gin.Context) {
|
||||
// 解析分页参数
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// 获取用户ID
|
||||
cuUserID, exists := c.Get("cu_user_id")
|
||||
if !exists {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("100004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
orders, total, err := h.cuOrderService.GetUserOrders(c.Request.Context(), cuUserID.(string), offset, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(err.(*i18n.I18nError).HttpCode, models.ErrorResponse{
|
||||
Code: err.(*i18n.I18nError).Code,
|
||||
Message: err.(*i18n.I18nError).Message, // 直接使用错误对象中的消息
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
orderResponses := make([]*models.CuOrderResponse, len(orders))
|
||||
for i, order := range orders {
|
||||
orderResponses[i] = order.ToResponse()
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMsg := i18n.GetI18nErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMsg,
|
||||
Data: &models.CuOrderListResponse{
|
||||
Orders: orderResponses,
|
||||
TotalCount: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCuOrderHandler_getPackageList(t *testing.T) {
|
||||
handler := &CuOrderHandler{}
|
||||
|
||||
packages := handler.getPackageList()
|
||||
|
||||
// 验证返回了3种套餐
|
||||
assert.Equal(t, 3, len(packages))
|
||||
|
||||
// 验证试用版
|
||||
trial := packages[0]
|
||||
assert.Equal(t, "trial", trial["id"])
|
||||
assert.Equal(t, "试用版", trial["name"])
|
||||
assert.Equal(t, 0, trial["price"])
|
||||
assert.Equal(t, 1, trial["max_devices"])
|
||||
|
||||
// 验证基础版
|
||||
basic := packages[1]
|
||||
assert.Equal(t, "basic", basic["id"])
|
||||
assert.Equal(t, "基础版", basic["name"])
|
||||
assert.Equal(t, 300, basic["price"])
|
||||
|
||||
// 验证专业版
|
||||
professional := packages[2]
|
||||
assert.Equal(t, "professional", professional["id"])
|
||||
assert.Equal(t, "专业版", professional["name"])
|
||||
assert.Equal(t, 2000, professional["price"])
|
||||
}
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"license-manager/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -230,4 +231,4 @@ func CustomerAuth() gin.HandlerFunc {
|
||||
// getCurrentTimestamp 获取当前时间戳
|
||||
func getCurrentTimestamp() string {
|
||||
return "2024-07-30T12:00:00Z" // 简化实现,实际应该使用time.Now().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ func SetupRouter() *gin.Engine {
|
||||
customerRepo := repository.NewCustomerRepository(db)
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
cuUserRepo := repository.NewCuUserRepository(db)
|
||||
cuOrderRepo := repository.NewCuOrderRepository(db)
|
||||
authCodeRepo := repository.NewAuthorizationCodeRepository(db)
|
||||
licenseRepo := repository.NewLicenseRepository(db)
|
||||
dashboardRepo := repository.NewDashboardRepository(db)
|
||||
@@ -51,8 +52,9 @@ func SetupRouter() *gin.Engine {
|
||||
systemService := service.NewSystemService()
|
||||
customerService := service.NewCustomerService(customerRepo)
|
||||
cuUserService := service.NewCuUserService(cuUserRepo, customerRepo, db)
|
||||
enumService := service.NewEnumService()
|
||||
authCodeService := service.NewAuthorizationCodeService(authCodeRepo, customerRepo, licenseRepo)
|
||||
cuOrderService := service.NewCuOrderService(cuOrderRepo, cuUserRepo, authCodeRepo, db)
|
||||
enumService := service.NewEnumService()
|
||||
licenseService := service.NewLicenseService(licenseRepo, db, log)
|
||||
dashboardService := service.NewDashboardService(dashboardRepo)
|
||||
|
||||
@@ -62,6 +64,7 @@ func SetupRouter() *gin.Engine {
|
||||
customerHandler := handlers.NewCustomerHandler(customerService)
|
||||
cuAuthHandler := handlers.NewCuAuthHandler(cuUserService)
|
||||
cuProfileHandler := handlers.NewCuProfileHandler(cuUserService)
|
||||
cuOrderHandler := handlers.NewCuOrderHandler(cuOrderService)
|
||||
enumHandler := handlers.NewEnumHandler(enumService)
|
||||
authCodeHandler := handlers.NewAuthorizationCodeHandler(authCodeService)
|
||||
licenseHandler := handlers.NewLicenseHandler(licenseService)
|
||||
@@ -149,6 +152,7 @@ func SetupRouter() *gin.Engine {
|
||||
cuPublic.POST("/login", cuAuthHandler.CuUserLogin)
|
||||
cuPublic.POST("/forgot-password", cuAuthHandler.CuUserForgotPassword)
|
||||
cuPublic.POST("/reset-password", cuAuthHandler.CuUserResetPassword)
|
||||
cuPublic.GET("/packages", cuOrderHandler.GetPackages)
|
||||
}
|
||||
|
||||
// 需要认证的接口
|
||||
@@ -160,6 +164,12 @@ func SetupRouter() *gin.Engine {
|
||||
cuAuth.PUT("/profile", cuProfileHandler.UpdateCuUserProfile)
|
||||
cuAuth.PUT("/profile/phone", cuProfileHandler.UpdateCuUserPhone)
|
||||
cuAuth.PUT("/profile/password", cuProfileHandler.ChangeCuUserPassword)
|
||||
|
||||
// 套餐和订单管理
|
||||
cuAuth.POST("/orders/calculate", cuOrderHandler.CalculatePrice)
|
||||
cuAuth.POST("/orders", cuOrderHandler.CreateOrder)
|
||||
cuAuth.GET("/orders/:order_id", cuOrderHandler.GetOrder)
|
||||
cuAuth.GET("/orders", cuOrderHandler.GetUserOrders)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package database
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"license-manager/internal/config"
|
||||
@@ -25,6 +24,7 @@ func AutoMigrate() error {
|
||||
&models.CustomerCodeSequence{},
|
||||
&models.User{},
|
||||
&models.CuUser{},
|
||||
&models.CuOrder{},
|
||||
&models.AuthorizationCode{},
|
||||
&models.License{},
|
||||
&models.AuthorizationChange{},
|
||||
@@ -96,26 +96,9 @@ func createCuUserIndexes() error {
|
||||
|
||||
switch dbType {
|
||||
case "mysql":
|
||||
// 创建复合索引用于优化查询性能
|
||||
// 使用兼容所有MySQL版本的语法(不支持IF NOT EXISTS的版本会忽略重复错误)
|
||||
indexes := []string{
|
||||
"CREATE INDEX idx_cu_users_phone ON cu_users(phone, phone_country_code)",
|
||||
"CREATE INDEX idx_cu_users_customer_status ON cu_users(customer_id, status)",
|
||||
"CREATE INDEX idx_cu_users_status_locked ON cu_users(status, locked_until)",
|
||||
}
|
||||
|
||||
for _, indexSQL := range indexes {
|
||||
if err := DB.Exec(indexSQL).Error; err != nil {
|
||||
// 检查是否是索引已存在的错误(MySQL错误码1061),如果是则忽略
|
||||
if strings.Contains(err.Error(), "Duplicate key name") || strings.Contains(err.Error(), "already exists") {
|
||||
// 索引已存在,跳过
|
||||
continue
|
||||
}
|
||||
log.Printf("Warning: failed to create index: %v", err)
|
||||
// 不返回错误,允许索引创建失败(可能已存在)
|
||||
}
|
||||
}
|
||||
log.Println("Created cu_users composite indexes")
|
||||
// 注意:所有必要的索引都已在SQL迁移文件中创建
|
||||
// 这里不再重复创建,避免"Duplicate key name"错误
|
||||
log.Println("All cu_users indexes already created in SQL migration file")
|
||||
default:
|
||||
log.Printf("Skipping composite index creation for database type: %s", dbType)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ type UserInfo struct {
|
||||
// AuthorizationCode 授权码模型
|
||||
type AuthorizationCode struct {
|
||||
ID string `gorm:"type:varchar(36);primaryKey" json:"id"` // 授权码ID
|
||||
Code string `gorm:"type:varchar(100);uniqueIndex;not null" json:"code"` // 授权码
|
||||
Code string `gorm:"type:varchar(1000);not null" json:"code"` // 授权码
|
||||
CustomerID string `gorm:"type:varchar(36);not null;index" json:"customer_id"` // 客户ID
|
||||
CustomerName string `gorm:"-" json:"customer_name,omitempty"` // 客户名称
|
||||
CustomerNameDisplay string `gorm:"-" json:"customer_name_display,omitempty"` // 客户名称显示(多语言)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CuOrder 客户用户订单模型
|
||||
type CuOrder struct {
|
||||
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
|
||||
OrderNo string `gorm:"type:varchar(50);not null;uniqueIndex" json:"order_no"`
|
||||
CustomerID string `gorm:"type:varchar(36);not null;index" json:"customer_id"`
|
||||
CuUserID string `gorm:"type:varchar(36);not null;index" json:"cu_user_id"`
|
||||
PackageID string `gorm:"type:varchar(50);not null;index" json:"package_id"`
|
||||
PackageName string `gorm:"type:varchar(100);not null" json:"package_name"`
|
||||
LicenseCount int `gorm:"not null" json:"license_count"`
|
||||
UnitPrice float64 `gorm:"type:decimal(10,2);not null" json:"unit_price"`
|
||||
DiscountRate float64 `gorm:"type:decimal(3,2);not null;default:1.0" json:"discount_rate"`
|
||||
TotalAmount float64 `gorm:"type:decimal(10,2);not null" json:"total_amount"`
|
||||
Status string `gorm:"type:varchar(20);not null;default:'paid'" json:"status"`
|
||||
AuthorizationCode *string `gorm:"type:varchar(500)" json:"authorization_code"`
|
||||
ExpiredAt *time.Time `gorm:"index" json:"expired_at"`
|
||||
CreatedAt time.Time `gorm:"not null;index" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (CuOrder) TableName() string {
|
||||
return "cu_orders"
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前自动设置时间戳和ID
|
||||
func (o *CuOrder) BeforeCreate(tx *gorm.DB) error {
|
||||
// 生成UUID作为主键ID
|
||||
if o.ID == "" {
|
||||
o.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// 设置时间戳
|
||||
now := time.Now()
|
||||
if o.CreatedAt.IsZero() {
|
||||
o.CreatedAt = now
|
||||
}
|
||||
if o.UpdatedAt.IsZero() {
|
||||
o.UpdatedAt = now
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate 更新前自动设置时间戳
|
||||
func (o *CuOrder) BeforeUpdate(tx *gorm.DB) error {
|
||||
o.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CuOrderCreateRequest 创建订单请求结构
|
||||
type CuOrderCreateRequest struct {
|
||||
PackageID string `json:"package_id" binding:"required"`
|
||||
LicenseCount int `json:"license_count" binding:"required,min=1,max=1000"`
|
||||
}
|
||||
|
||||
// CuOrderResponse 订单响应结构
|
||||
type CuOrderResponse struct {
|
||||
ID string `json:"id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
CuUserID string `json:"cu_user_id"`
|
||||
PackageID string `json:"package_id"`
|
||||
PackageName string `json:"package_name"`
|
||||
LicenseCount int `json:"license_count"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
DiscountRate float64 `json:"discount_rate"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
Status string `json:"status"`
|
||||
AuthorizationCode *string `json:"authorization_code"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ToResponse 转换为响应结构
|
||||
func (o *CuOrder) ToResponse() *CuOrderResponse {
|
||||
return &CuOrderResponse{
|
||||
ID: o.ID,
|
||||
OrderNo: o.OrderNo,
|
||||
CustomerID: o.CustomerID,
|
||||
CuUserID: o.CuUserID,
|
||||
PackageID: o.PackageID,
|
||||
PackageName: o.PackageName,
|
||||
LicenseCount: o.LicenseCount,
|
||||
UnitPrice: o.UnitPrice,
|
||||
DiscountRate: o.DiscountRate,
|
||||
TotalAmount: o.TotalAmount,
|
||||
Status: o.Status,
|
||||
AuthorizationCode: o.AuthorizationCode,
|
||||
ExpiredAt: o.ExpiredAt,
|
||||
CreatedAt: o.CreatedAt,
|
||||
UpdatedAt: o.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// CuOrderListResponse 订单列表响应结构
|
||||
type CuOrderListResponse struct {
|
||||
Orders []*CuOrderResponse `json:"orders"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"license-manager/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CuOrderRepository interface {
|
||||
Create(order *models.CuOrder) error
|
||||
GetByID(id string) (*models.CuOrder, error)
|
||||
GetByOrderNo(orderNo string) (*models.CuOrder, error)
|
||||
GetByCuUserID(cuUserID string, offset, limit int) ([]*models.CuOrder, int64, error)
|
||||
GetByCustomerID(customerID string, offset, limit int) ([]*models.CuOrder, int64, error)
|
||||
Update(order *models.CuOrder) error
|
||||
Delete(id string) error
|
||||
CheckTrialOrderExists(cuUserID string, currentMonth string) (bool, error)
|
||||
}
|
||||
|
||||
type cuOrderRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCuOrderRepository(db *gorm.DB) CuOrderRepository {
|
||||
return &cuOrderRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *cuOrderRepository) Create(order *models.CuOrder) error {
|
||||
return r.db.Create(order).Error
|
||||
}
|
||||
|
||||
func (r *cuOrderRepository) GetByID(id string) (*models.CuOrder, error) {
|
||||
var order models.CuOrder
|
||||
err := r.db.Where("id = ? AND deleted_at IS NULL", id).First(&order).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *cuOrderRepository) GetByOrderNo(orderNo string) (*models.CuOrder, error) {
|
||||
var order models.CuOrder
|
||||
err := r.db.Where("order_no = ? AND deleted_at IS NULL", orderNo).First(&order).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *cuOrderRepository) GetByCuUserID(cuUserID string, offset, limit int) ([]*models.CuOrder, int64, error) {
|
||||
var orders []*models.CuOrder
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.CuOrder{}).Where("cu_user_id = ? AND deleted_at IS NULL", cuUserID)
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&orders).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return orders, total, nil
|
||||
}
|
||||
|
||||
func (r *cuOrderRepository) GetByCustomerID(customerID string, offset, limit int) ([]*models.CuOrder, int64, error) {
|
||||
var orders []*models.CuOrder
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.CuOrder{}).Where("customer_id = ? AND deleted_at IS NULL", customerID)
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&orders).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return orders, total, nil
|
||||
}
|
||||
|
||||
func (r *cuOrderRepository) Update(order *models.CuOrder) error {
|
||||
return r.db.Save(order).Error
|
||||
}
|
||||
|
||||
func (r *cuOrderRepository) Delete(id string) error {
|
||||
return r.db.Where("id = ?", id).Delete(&models.CuOrder{}).Error
|
||||
}
|
||||
|
||||
// GetPendingOrders 移除:不再有pending状态的订单
|
||||
|
||||
// UpdateStatus 移除:不再需要更新订单状态
|
||||
|
||||
func (r *cuOrderRepository) CheckTrialOrderExists(cuUserID string, currentMonth string) (bool, error) {
|
||||
var count int64
|
||||
startOfMonth := currentMonth + "-01"
|
||||
endOfMonth := currentMonth + "-31"
|
||||
|
||||
err := r.db.Model(&models.CuOrder{}).
|
||||
Where("cu_user_id = ? AND package_id = ? AND status = ? AND created_at >= ? AND created_at <= ? AND deleted_at IS NULL",
|
||||
cuUserID, "trial", "paid", startOfMonth, endOfMonth).
|
||||
Count(&count).Error
|
||||
|
||||
return count > 0, err
|
||||
}
|
||||
@@ -10,10 +10,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/repository"
|
||||
pkgcontext "license-manager/pkg/context"
|
||||
"license-manager/pkg/i18n"
|
||||
"license-manager/pkg/utils"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type authorizationCodeService struct {
|
||||
@@ -69,13 +73,40 @@ func (s *authorizationCodeService) CreateAuthorizationCode(ctx context.Context,
|
||||
return nil, i18n.NewI18nError("100004", lang) // 缺少认证信息
|
||||
}
|
||||
|
||||
// 生成授权码
|
||||
authCode, err := s.generateAuthorizationCode(req.CustomerID)
|
||||
// 生成自包含授权码
|
||||
var featureConfigMap, usageLimitsMap, customParametersMap map[string]interface{}
|
||||
|
||||
if req.FeatureConfig != nil {
|
||||
if fc, ok := req.FeatureConfig.(map[string]interface{}); ok {
|
||||
featureConfigMap = fc
|
||||
}
|
||||
}
|
||||
if req.UsageLimits != nil {
|
||||
if ul, ok := req.UsageLimits.(map[string]interface{}); ok {
|
||||
usageLimitsMap = ul
|
||||
}
|
||||
}
|
||||
if req.CustomParameters != nil {
|
||||
if cp, ok := req.CustomParameters.(map[string]interface{}); ok {
|
||||
customParametersMap = cp
|
||||
}
|
||||
}
|
||||
|
||||
licenseConfig := &utils.LicenseConfig{
|
||||
EndDate: endDate,
|
||||
FeatureConfig: featureConfigMap,
|
||||
UsageLimits: usageLimitsMap,
|
||||
CustomParameters: customParametersMap,
|
||||
}
|
||||
|
||||
// 从配置获取RSA私钥路径
|
||||
privateKeyPath := config.GetConfig().License.RSA.PrivateKeyPath
|
||||
authCode, err := utils.EncodeLicenseData(licenseConfig, privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 处理JSON字段
|
||||
// 处理JSON字段(用于数据库存储)
|
||||
var featureConfig, usageLimits, customParameters models.JSON
|
||||
if req.FeatureConfig != nil {
|
||||
featureConfigBytes, err := json.Marshal(req.FeatureConfig)
|
||||
@@ -108,6 +139,7 @@ func (s *authorizationCodeService) CreateAuthorizationCode(ctx context.Context,
|
||||
|
||||
// 构建授权码实体
|
||||
authCodeEntity := &models.AuthorizationCode{
|
||||
ID: uuid.New().String(), // 生成新的UUID作为主键
|
||||
Code: authCode,
|
||||
CustomerID: req.CustomerID,
|
||||
CreatedBy: currentUserID,
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/repository"
|
||||
pkgcontext "license-manager/pkg/context"
|
||||
"license-manager/pkg/i18n"
|
||||
"license-manager/pkg/utils"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CuOrderService interface {
|
||||
CreateOrder(ctx context.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) (*models.CuOrder, error)
|
||||
GetOrder(ctx context.Context, orderID, cuUserID string) (*models.CuOrder, error)
|
||||
GetUserOrders(ctx context.Context, cuUserID string, offset, limit int) ([]*models.CuOrder, int64, error)
|
||||
CalculatePrice(ctx context.Context, packageID string, licenseCount int) (*PriceCalculationResult, error)
|
||||
}
|
||||
|
||||
type PriceCalculationResult struct {
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
LicenseCount int `json:"license_count"`
|
||||
DiscountRate float64 `json:"discount_rate"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
DiscountDesc string `json:"discount_description"`
|
||||
}
|
||||
|
||||
// 套餐配置(临时写死,后续可改为配置化)
|
||||
var packageConfigs = map[string]PackageConfig{
|
||||
"trial": {
|
||||
Name: "试用版",
|
||||
Price: 0,
|
||||
MaxDevices: 1, // 试用版限制1个许可
|
||||
},
|
||||
"basic": {
|
||||
Name: "基础版",
|
||||
Price: 300,
|
||||
MaxDevices: 1000, // 基础版最多1000个许可
|
||||
},
|
||||
"professional": {
|
||||
Name: "专业版",
|
||||
Price: 2000,
|
||||
MaxDevices: 1000, // 专业版最多1000个许可
|
||||
},
|
||||
}
|
||||
|
||||
type PackageConfig struct {
|
||||
Name string
|
||||
Price float64
|
||||
MaxDevices int
|
||||
}
|
||||
|
||||
// 折扣规则配置
|
||||
var discountRules = []DiscountRule{
|
||||
{MinQuantity: 50, MaxQuantity: 99, Rate: 0.9, Description: "50-99许可9折优惠"},
|
||||
{MinQuantity: 100, MaxQuantity: 499, Rate: 0.8, Description: "100-499许可8折优惠"},
|
||||
{MinQuantity: 500, MaxQuantity: 0, Rate: 0.7, Description: "500+许可7折优惠"},
|
||||
}
|
||||
|
||||
type DiscountRule struct {
|
||||
MinQuantity int
|
||||
MaxQuantity int // 0表示无上限
|
||||
Rate float64
|
||||
Description string
|
||||
}
|
||||
|
||||
type cuOrderService struct {
|
||||
repo repository.CuOrderRepository
|
||||
cuUserRepo repository.CuUserRepository
|
||||
authCodeRepo repository.AuthorizationCodeRepository
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCuOrderService(repo repository.CuOrderRepository, cuUserRepo repository.CuUserRepository, authCodeRepo repository.AuthorizationCodeRepository, db *gorm.DB) CuOrderService {
|
||||
return &cuOrderService{
|
||||
repo: repo,
|
||||
cuUserRepo: cuUserRepo,
|
||||
authCodeRepo: authCodeRepo,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *cuOrderService) CreateOrder(ctx context.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) (*models.CuOrder, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 参数验证
|
||||
if req == nil || req.PackageID == "" || req.LicenseCount <= 0 {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 检查套餐是否存在
|
||||
pkg, exists := packageConfigs[req.PackageID]
|
||||
if !exists {
|
||||
return nil, i18n.NewI18nError("600001", lang) // 套餐不存在
|
||||
}
|
||||
|
||||
// 验证许可数量
|
||||
if req.LicenseCount > pkg.MaxDevices {
|
||||
return nil, i18n.NewI18nError("601005", lang) // 许可数量超出限制
|
||||
}
|
||||
|
||||
// 试用版特殊检查
|
||||
if req.PackageID == "trial" {
|
||||
// 试用版不支持批量购买
|
||||
if req.LicenseCount != 1 {
|
||||
return nil, i18n.NewI18nError("600004", lang) // 试用版不支持批量购买
|
||||
}
|
||||
|
||||
// 检查购买时间限制
|
||||
now := time.Now()
|
||||
if now.Day() > 25 {
|
||||
return nil, i18n.NewI18nError("600003", lang) // 试用版购买时间限制
|
||||
}
|
||||
|
||||
// 每个用户每月只能有一个试用版
|
||||
currentMonth := now.Format("2006-01")
|
||||
exists, err := s.repo.CheckTrialOrderExists(cuUserID, currentMonth)
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
if exists {
|
||||
return nil, i18n.NewI18nError("600005", lang) // 当月已购买试用版
|
||||
}
|
||||
}
|
||||
|
||||
// 计算价格
|
||||
priceResult, err := s.CalculatePrice(ctx, req.PackageID, req.LicenseCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
orderNo := s.generateOrderNo()
|
||||
|
||||
// 创建订单
|
||||
order := &models.CuOrder{
|
||||
OrderNo: orderNo,
|
||||
CustomerID: customerID,
|
||||
CuUserID: cuUserID,
|
||||
PackageID: req.PackageID,
|
||||
PackageName: pkg.Name,
|
||||
LicenseCount: req.LicenseCount,
|
||||
UnitPrice: priceResult.UnitPrice,
|
||||
DiscountRate: priceResult.DiscountRate,
|
||||
TotalAmount: priceResult.TotalAmount,
|
||||
Status: "paid", // 直接设置为已支付状态
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 创建订单
|
||||
if err := tx.Create(order).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 支付成功,创建授权码
|
||||
// 实际项目中这里应该先调用支付接口,支付成功后再执行后续逻辑
|
||||
now := time.Now()
|
||||
order.UpdatedAt = now
|
||||
|
||||
// 计算授权时间
|
||||
var startDate, endDate time.Time
|
||||
var expiredAt *time.Time
|
||||
if req.PackageID == "trial" {
|
||||
// 试用版:从今天到当月25日
|
||||
trialExpiry := time.Date(now.Year(), now.Month(), 25, 23, 59, 59, 0, now.Location())
|
||||
if now.Day() > 25 {
|
||||
// 如果今天已经过了25日,则无法购买
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("600003", lang) // 试用版购买时间限制
|
||||
}
|
||||
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
endDate = trialExpiry
|
||||
expiredAt = &trialExpiry
|
||||
} else {
|
||||
// 永久版:从今天开始,结束时间设为很远的未来
|
||||
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
endDate = startDate.AddDate(1000, 0, 0) // 1000年后
|
||||
expiredAt = nil
|
||||
}
|
||||
|
||||
// 生成自包含授权码
|
||||
var featureConfigMap, usageLimitsMap map[string]interface{}
|
||||
|
||||
// 根据套餐类型设置配置信息
|
||||
if req.PackageID == "basic" {
|
||||
usageLimitsMap = map[string]interface{}{
|
||||
"type": "standard",
|
||||
}
|
||||
}
|
||||
|
||||
licenseConfig := &utils.LicenseConfig{
|
||||
EndDate: endDate,
|
||||
FeatureConfig: featureConfigMap,
|
||||
UsageLimits: usageLimitsMap,
|
||||
CustomParameters: map[string]interface{}{
|
||||
"package_id": req.PackageID,
|
||||
"package_name": pkg.Name,
|
||||
"license_count": req.LicenseCount,
|
||||
},
|
||||
}
|
||||
|
||||
// 从配置获取RSA私钥路径
|
||||
privateKeyPath := config.GetConfig().License.RSA.PrivateKeyPath
|
||||
authCode, err := utils.EncodeLicenseData(licenseConfig, privateKeyPath)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 构建订单描述
|
||||
description := fmt.Sprintf("%s - %d个许可", pkg.Name, req.LicenseCount)
|
||||
|
||||
// 构建授权码实体(使用生成的授权码)
|
||||
authCodeEntity := &models.AuthorizationCode{
|
||||
ID: uuid.New().String(), // 生成新的UUID作为主键
|
||||
Code: authCode,
|
||||
CustomerID: customerID,
|
||||
CreatedBy: cuUserID,
|
||||
Description: &description,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
DeploymentType: "cloud",
|
||||
EncryptionType: &[]string{"standard"}[0],
|
||||
MaxActivations: req.LicenseCount,
|
||||
IsLocked: false,
|
||||
// FeatureConfig 和 UsageLimits 已在授权码字符串中,无需重复存储
|
||||
}
|
||||
|
||||
// 创建授权码
|
||||
if err := s.authCodeRepo.CreateAuthorizationCode(ctx, authCodeEntity); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 更新订单的授权码信息
|
||||
order.AuthorizationCode = &authCode
|
||||
order.ExpiredAt = expiredAt
|
||||
|
||||
// 更新订单
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (s *cuOrderService) GetOrder(ctx context.Context, orderID, cuUserID string) (*models.CuOrder, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
order, err := s.repo.GetByID(orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, i18n.NewI18nError("601001", lang) // 订单不存在
|
||||
}
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 检查权限:只能查看自己的订单
|
||||
if order.CuUserID != cuUserID {
|
||||
return nil, i18n.NewI18nError("100005", lang) // 权限不足
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (s *cuOrderService) GetUserOrders(ctx context.Context, cuUserID string, offset, limit int) ([]*models.CuOrder, int64, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 参数验证
|
||||
if cuUserID == "" {
|
||||
return nil, 0, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
orders, total, err := s.repo.GetByCuUserID(cuUserID, offset, limit)
|
||||
if err != nil {
|
||||
return nil, 0, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
return orders, total, nil
|
||||
}
|
||||
|
||||
func (s *cuOrderService) CalculatePrice(ctx context.Context, packageID string, licenseCount int) (*PriceCalculationResult, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 检查套餐是否存在
|
||||
pkg, exists := packageConfigs[packageID]
|
||||
if !exists {
|
||||
return nil, i18n.NewI18nError("600001", lang) // 套餐不存在
|
||||
}
|
||||
|
||||
// 检查套餐是否已下架(虽然当前是写死配置,但保留这个检查)
|
||||
// TODO: 如果将来添加status字段,可以在这里检查
|
||||
// if pkg.Status != "active" {
|
||||
// return nil, i18n.NewI18nError("600002", lang) // 套餐已下架
|
||||
// }
|
||||
|
||||
// 验证许可数量
|
||||
if licenseCount > pkg.MaxDevices {
|
||||
return nil, i18n.NewI18nError("601005", lang) // 许可数量超出限制
|
||||
}
|
||||
|
||||
// 计算折扣
|
||||
discountRate := 1.0
|
||||
discountDesc := "不享受折扣"
|
||||
for _, rule := range discountRules {
|
||||
if licenseCount >= rule.MinQuantity && (rule.MaxQuantity == 0 || licenseCount <= rule.MaxQuantity) {
|
||||
discountRate = rule.Rate
|
||||
discountDesc = rule.Description
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总价
|
||||
totalAmount := pkg.Price * float64(licenseCount) * discountRate
|
||||
|
||||
return &PriceCalculationResult{
|
||||
UnitPrice: pkg.Price,
|
||||
LicenseCount: licenseCount,
|
||||
DiscountRate: discountRate,
|
||||
TotalAmount: totalAmount,
|
||||
DiscountDesc: discountDesc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
func (s *cuOrderService) generateOrderNo() string {
|
||||
now := time.Now()
|
||||
dateStr := now.Format("20060102")
|
||||
|
||||
// 使用纳秒时间戳的最后9位作为序列号,确保全局唯一性
|
||||
// 纳秒时间戳在同一纳秒内不会重复,重启应用也不会影响
|
||||
nano := now.UnixNano() % 1000000000 // 取最后9位
|
||||
|
||||
return fmt.Sprintf("ORD%s%09d", dateStr, nano)
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
CREATE TABLE authorization_codes (
|
||||
-- 基础信息属性
|
||||
id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
code VARCHAR(100) NOT NULL UNIQUE COMMENT '唯一授权码,软件身份凭证',
|
||||
code VARCHAR(1000) NOT NULL UNIQUE COMMENT '唯一授权码,软件身份凭证(支持自包含配置)',
|
||||
customer_id VARCHAR(36) NOT NULL COMMENT '关联客户ID',
|
||||
created_by VARCHAR(36) NOT NULL COMMENT '创建人ID',
|
||||
software_id VARCHAR(50) COMMENT '目标软件产品ID',
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
-- 客户用户订单表结构
|
||||
-- 基于产品套餐体系设计创建
|
||||
-- MySQL版本
|
||||
|
||||
-- 创建客户用户订单表
|
||||
CREATE TABLE cu_orders (
|
||||
-- 基础信息
|
||||
id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
order_no VARCHAR(50) NOT NULL COMMENT '订单号',
|
||||
customer_id VARCHAR(36) NOT NULL COMMENT '客户ID',
|
||||
cu_user_id VARCHAR(36) NOT NULL COMMENT '客户用户ID',
|
||||
package_id VARCHAR(50) NOT NULL COMMENT '套餐ID (对应配置中的ID)',
|
||||
package_name VARCHAR(100) NOT NULL COMMENT '套餐名称(快照)',
|
||||
|
||||
-- 购买信息
|
||||
license_count INT NOT NULL COMMENT '许可数量',
|
||||
unit_price DECIMAL(10,2) NOT NULL COMMENT '单价(每许可价格,快照)',
|
||||
discount_rate DECIMAL(3,2) NOT NULL DEFAULT 1.0 COMMENT '折扣率',
|
||||
total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
|
||||
|
||||
-- 状态信息
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'paid' COMMENT '订单状态: paid-已支付',
|
||||
authorization_code VARCHAR(50) NULL COMMENT '生成的授权码',
|
||||
|
||||
-- 时间信息
|
||||
expired_at DATETIME(3) NULL COMMENT '订单过期时间',
|
||||
created_at DATETIME(3) NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL,
|
||||
deleted_at DATETIME(3) NULL,
|
||||
|
||||
-- 外键约束
|
||||
CONSTRAINT fk_cu_orders_customer_id FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_cu_orders_cu_user_id FOREIGN KEY (cu_user_id) REFERENCES cu_users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户用户订单表 - 产品套餐购买订单';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_cu_orders_customer_id ON cu_orders(customer_id);
|
||||
CREATE INDEX idx_cu_orders_cu_user_id ON cu_orders(cu_user_id);
|
||||
CREATE INDEX idx_cu_orders_status ON cu_orders(status);
|
||||
CREATE INDEX idx_cu_orders_created_at ON cu_orders(created_at);
|
||||
CREATE INDEX idx_cu_orders_package_id ON cu_orders(package_id);
|
||||
CREATE INDEX idx_cu_orders_deleted_at ON cu_orders(deleted_at);
|
||||
|
||||
-- 注意:
|
||||
-- 1. 时间戳由Go应用程序管理,不依赖数据库默认值
|
||||
-- 2. license_count决定授权码可以激活多少次
|
||||
-- 3. authorization_code在订单支付后生成
|
||||
-- 4. 使用软删除,通过deleted_at字段实现
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 更新授权码表code字段长度以支持自包含配置
|
||||
-- 使用HMAC签名的极简格式,预计长度80-200字符,预留足够空间
|
||||
|
||||
-- MySQL
|
||||
-- 删除现有的UNIQUE索引(基于code字段的唯一索引)
|
||||
-- 注意:如果索引不存在会报错但不影响迁移,可以忽略
|
||||
ALTER TABLE authorization_codes DROP INDEX IF EXISTS idx_authorization_codes_code;
|
||||
|
||||
-- 修改字段长度为1000字符以支持更长的授权码
|
||||
ALTER TABLE authorization_codes MODIFY COLUMN code varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL;
|
||||
|
||||
-- 创建前缀UNIQUE索引(只索引前255个字符,足够区分授权码)
|
||||
-- 这样既保证唯一性,又避免了InnoDB的索引长度限制(3072字节)
|
||||
CREATE UNIQUE INDEX idx_authorization_codes_code_prefix ON authorization_codes(code(255));
|
||||
|
||||
-- 注意事项:
|
||||
-- 1. 此迁移会更改现有数据结构,请在生产环境执行前做好备份
|
||||
-- 2. 前缀索引保证了唯一性,但只检查前255个字符的唯一性
|
||||
-- 3. 对于LIC-{随机}-{Base64数据}格式,前255字符的碰撞概率极低
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 更新cu_orders表authorization_code字段长度
|
||||
-- 支持新的HMAC签名授权码格式(约150-200字符)
|
||||
|
||||
-- MySQL
|
||||
-- 修改authorization_code字段长度从50到500字符
|
||||
ALTER TABLE cu_orders MODIFY COLUMN authorization_code varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 注意事项:
|
||||
-- 1. 此迁移会更改现有数据结构,请在生产环境执行前做好备份
|
||||
-- 2. 500字符长度足以支持当前和未来的授权码格式
|
||||
-- 3. 如果将来需要更长,仍然可以进一步扩展
|
||||
@@ -7,8 +7,16 @@
|
||||
```
|
||||
sql/
|
||||
├── migrations/ # 数据库迁移文件
|
||||
│ ├── 001_create_customers_table.sql # 客户表结构
|
||||
│ └── 002_insert_sample_data.sql # 示例数据
|
||||
│ ├── 001_create_customers_table.sql # 客户表结构
|
||||
│ ├── 002_insert_sample_data.sql # 示例数据
|
||||
│ ├── 003_create_authorization_codes_table.sql # 授权码表结构
|
||||
│ ├── 004_create_licenses_table.sql # 许可证表结构
|
||||
│ ├── 005_create_authorization_changes_table.sql # 授权变更表结构
|
||||
│ ├── 006_create_users_table.sql # 管理员用户表结构
|
||||
│ ├── 007_create_cu_users_table.sql # 客户用户表结构
|
||||
│ ├── 008_create_cu_orders_table.sql # 客户用户订单表结构
|
||||
│ ├── 009_update_authorization_codes_code_length.sql # 更新授权码字段长度
|
||||
│ └── 010_update_cu_orders_authorization_code_length.sql # 更新订单表授权码字段长度
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
@@ -35,6 +43,40 @@ sql/
|
||||
- 教育客户
|
||||
- 禁用状态客户
|
||||
|
||||
### 008_create_cu_orders_table.sql
|
||||
创建客户用户订单表结构,包括:
|
||||
|
||||
- **订单主表** (`cu_orders`):存储客户用户购买套餐的订单信息
|
||||
- **许可管理**:记录许可数量、价格、折扣等购买信息
|
||||
- **状态跟踪**:订单状态从创建到支付完成的生命周期
|
||||
- **授权关联**:存储生成的授权码和过期时间
|
||||
|
||||
### 009_update_authorization_codes_code_length.sql
|
||||
更新授权码表结构以支持自包含配置授权码:
|
||||
|
||||
- **字段更新**:`code VARCHAR(100) → VARCHAR(1000)`
|
||||
- **功能增强**:支持包含完整配置信息的自包含授权码
|
||||
- **向后兼容**:保持对现有短授权码的支持
|
||||
- **安全考虑**:RSA签名确保配置信息完整性
|
||||
|
||||
**执行注意事项**:
|
||||
- 此迁移会修改现有表结构
|
||||
- 生产环境执行前务必备份数据
|
||||
- 现有短授权码继续有效
|
||||
- 新授权码可包含完整配置信息
|
||||
|
||||
### 010_update_cu_orders_authorization_code_length.sql
|
||||
更新客户订单表授权码字段长度以支持新的HMAC签名授权码:
|
||||
|
||||
- **字段更新**:`authorization_code VARCHAR(50) → VARCHAR(500)`
|
||||
- **兼容性**:支持约150-200字符的新格式授权码
|
||||
- **安全升级**:从RSA签名升级到HMAC签名,显著缩短授权码长度
|
||||
|
||||
**执行注意事项**:
|
||||
- 扩展字段长度以适应新的授权码格式
|
||||
- 不会影响现有数据
|
||||
- 支持更高效的离线验证
|
||||
|
||||
## 执行顺序
|
||||
|
||||
按文件名的数字前缀顺序执行:
|
||||
|
||||
@@ -23,7 +23,8 @@ func NewI18nErrorResponse(code, lang string, customMessage ...string) (int, stri
|
||||
// 获取本地化错误信息
|
||||
message := GetI18nErrorMessage(code, lang)
|
||||
|
||||
// 支持自定义消息覆盖
|
||||
// 如果提供了自定义消息,则使用自定义消息
|
||||
// 这样可以让调用方根据需要显示具体错误信息或本地化消息
|
||||
if len(customMessage) > 0 && customMessage[0] != "" {
|
||||
message = customMessage[0]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LicenseConfig 自包含授权码配置结构(极简字段名)
|
||||
type LicenseConfig struct {
|
||||
EndDate time.Time `json:"e"` // 到期时间
|
||||
FeatureConfig map[string]interface{} `json:"f,omitempty"` // 功能配置
|
||||
UsageLimits map[string]interface{} `json:"l,omitempty"` // 使用限制
|
||||
CustomParameters map[string]interface{} `json:"p,omitempty"` // 自定义参数
|
||||
}
|
||||
|
||||
// EncodeLicenseData 将配置数据编码为自包含授权码(极简模式)
|
||||
func EncodeLicenseData(config *LicenseConfig, privateKeyPath string) (string, error) {
|
||||
if config == nil {
|
||||
return "", errors.New("config cannot be nil")
|
||||
}
|
||||
|
||||
// 使用极简紧凑序列化
|
||||
configBytes, err := ultraCompactMarshal(config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
// 使用HMAC-SHA256替代RSA签名(短签名模式)
|
||||
// 从私钥文件中提取密钥作为HMAC密钥(简化处理)
|
||||
hmacKey, err := getHMACKeyFromRSAKey(privateKeyPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get HMAC key: %w", err)
|
||||
}
|
||||
|
||||
signature, err := signWithHMAC(configBytes, hmacKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign data: %w", err)
|
||||
}
|
||||
|
||||
// 极简格式:[版本(4位)][时间戳(32位)][配置数据][HMAC签名(32字节)]
|
||||
version := uint8(3) // 版本3(极简模式)
|
||||
timestamp := uint32(config.EndDate.Unix())
|
||||
|
||||
// 打包头部:版本(4位) + 时间戳(28位) 到5字节
|
||||
header := make([]byte, 5)
|
||||
header[0] = (version << 4) | byte(timestamp>>28)
|
||||
header[1] = byte(timestamp >> 20)
|
||||
header[2] = byte(timestamp >> 12)
|
||||
header[3] = byte(timestamp >> 4)
|
||||
header[4] = byte(timestamp << 4) // 最后4位保留给标志位
|
||||
|
||||
// 设置字段存在标志
|
||||
flagBits := uint8(0)
|
||||
if config.FeatureConfig != nil {
|
||||
flagBits |= 1 << 0
|
||||
}
|
||||
if config.UsageLimits != nil {
|
||||
flagBits |= 1 << 1
|
||||
}
|
||||
if config.CustomParameters != nil {
|
||||
flagBits |= 1 << 2
|
||||
}
|
||||
header[4] |= flagBits
|
||||
|
||||
// 构建数据:[头部(5字节)][配置数据][签名(32字节)]
|
||||
data := append(header, configBytes...)
|
||||
data = append(data, signature...)
|
||||
|
||||
// 使用URL安全的Base64编码
|
||||
encodedData := base64.RawURLEncoding.EncodeToString(data)
|
||||
|
||||
// 生成随机码(2位)
|
||||
randomStr, err := generateRandomString(2)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random string: %w", err)
|
||||
}
|
||||
|
||||
// 最终格式:LIC-{2位随机}-{数据}
|
||||
return fmt.Sprintf("LIC-%s-%s", randomStr, encodedData), nil
|
||||
}
|
||||
|
||||
// ultraCompactMarshal 极简紧凑序列化(无长度前缀)
|
||||
func ultraCompactMarshal(config *LicenseConfig) ([]byte, error) {
|
||||
var buf []byte
|
||||
|
||||
// 直接序列化存在的字段,不使用长度前缀
|
||||
// 依赖解码时知道字段顺序和格式
|
||||
|
||||
if config.FeatureConfig != nil {
|
||||
mapBytes, err := json.Marshal(config.FeatureConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, mapBytes...)
|
||||
buf = append(buf, 0) // 字段分隔符
|
||||
}
|
||||
|
||||
if config.UsageLimits != nil {
|
||||
mapBytes, err := json.Marshal(config.UsageLimits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, mapBytes...)
|
||||
buf = append(buf, 0) // 字段分隔符
|
||||
}
|
||||
|
||||
if config.CustomParameters != nil {
|
||||
mapBytes, err := json.Marshal(config.CustomParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, mapBytes...)
|
||||
buf = append(buf, 0) // 字段分隔符
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// getHMACKeyFromRSAKey 从RSA密钥文件提取HMAC密钥
|
||||
func getHMACKeyFromRSAKey(keyPath string) ([]byte, error) {
|
||||
// 首先尝试作为私钥加载
|
||||
if privateKey, err := getLicensePrivateKey(keyPath); err == nil {
|
||||
// 使用私钥的模数作为HMAC密钥
|
||||
key := privateKey.N.Bytes()
|
||||
// 截取前32字节作为HMAC-SHA256密钥
|
||||
if len(key) > 32 {
|
||||
key = key[:32]
|
||||
} else if len(key) < 32 {
|
||||
// 补齐到32字节
|
||||
padding := make([]byte, 32-len(key))
|
||||
key = append(key, padding...)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// 如果私钥加载失败,尝试作为公钥加载
|
||||
if publicKey, err := getLicensePublicKey(keyPath); err == nil {
|
||||
// 使用公钥的模数作为HMAC密钥
|
||||
key := publicKey.N.Bytes()
|
||||
// 截取前32字节作为HMAC-SHA256密钥
|
||||
if len(key) > 32 {
|
||||
key = key[:32]
|
||||
} else if len(key) < 32 {
|
||||
// 补齐到32字节
|
||||
padding := make([]byte, 32-len(key))
|
||||
key = append(key, padding...)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("failed to load RSA key for HMAC")
|
||||
}
|
||||
|
||||
// getLicensePublicKey 获取RSA公钥(复用crypto.go的函数)
|
||||
func getLicensePublicKey(publicKeyPath string) (*RSAPublicKey, error) {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(publicKeyPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("RSA公钥文件不存在: %s", publicKeyPath)
|
||||
}
|
||||
|
||||
// 加载公钥
|
||||
publicKey, err := LoadRSAPublicKeyFromFile(publicKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RSA公钥文件损坏或格式错误: %w", err)
|
||||
}
|
||||
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// signWithHMAC 使用HMAC-SHA256签名
|
||||
func signWithHMAC(data []byte, key []byte) ([]byte, error) {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write(data)
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
// ultraCompactUnmarshal 从极简格式反序列化配置
|
||||
func ultraCompactUnmarshal(data []byte, flags uint8) (*LicenseConfig, error) {
|
||||
config := &LicenseConfig{}
|
||||
offset := 0
|
||||
|
||||
// 解析f字段(如果存在)
|
||||
if flags&1 != 0 {
|
||||
end := findFieldEnd(data, offset)
|
||||
if end == -1 {
|
||||
return nil, errors.New("invalid feature config format")
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data[offset:end], &m); err != nil {
|
||||
return nil, fmt.Errorf("invalid feature config: %w", err)
|
||||
}
|
||||
config.FeatureConfig = m
|
||||
offset = end + 1
|
||||
}
|
||||
|
||||
// 解析l字段(如果存在)
|
||||
if flags&(1<<1) != 0 {
|
||||
end := findFieldEnd(data, offset)
|
||||
if end == -1 {
|
||||
return nil, errors.New("invalid usage limits format")
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data[offset:end], &m); err != nil {
|
||||
return nil, fmt.Errorf("invalid usage limits: %w", err)
|
||||
}
|
||||
config.UsageLimits = m
|
||||
offset = end + 1
|
||||
}
|
||||
|
||||
// 解析p字段(如果存在)
|
||||
if flags&(1<<2) != 0 {
|
||||
if offset >= len(data) {
|
||||
return nil, errors.New("missing custom parameters")
|
||||
}
|
||||
// 最后一个字段到数据结尾
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data[offset:], &m); err != nil {
|
||||
return nil, fmt.Errorf("invalid custom parameters: %w", err)
|
||||
}
|
||||
config.CustomParameters = m
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// findFieldEnd 查找字段结束位置(以null字节分隔)
|
||||
func findFieldEnd(data []byte, start int) int {
|
||||
for i := start; i < len(data); i++ {
|
||||
if data[i] == 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// DecodeLicenseData 从授权码解析配置数据
|
||||
func DecodeLicenseData(code string, publicKeyPath string) (*LicenseConfig, error) {
|
||||
if !strings.HasPrefix(code, "LIC-") {
|
||||
return nil, errors.New("invalid license format")
|
||||
}
|
||||
|
||||
// 解析授权码格式:LIC-{2位随机码}-{数据}
|
||||
parts := strings.Split(code, "-")
|
||||
if len(parts) != 3 || parts[0] != "LIC" {
|
||||
return nil, errors.New("invalid license format")
|
||||
}
|
||||
|
||||
randomStr := parts[1]
|
||||
encodedData := parts[2]
|
||||
|
||||
// 验证随机码长度
|
||||
if len(randomStr) != 2 {
|
||||
return nil, errors.New("invalid random string length")
|
||||
}
|
||||
|
||||
// 使用URL安全的Base64解码
|
||||
dataBytes, err := base64.RawURLEncoding.DecodeString(encodedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode base64: %w", err)
|
||||
}
|
||||
|
||||
// 解析极简格式:[头部(5字节)][配置数据][签名(32字节)]
|
||||
if len(dataBytes) < 5+32 { // 头部 + 最少签名
|
||||
return nil, errors.New("invalid data format")
|
||||
}
|
||||
|
||||
// 解析头部
|
||||
header := dataBytes[:5]
|
||||
version := header[0] >> 4
|
||||
timestamp := uint32(header[0]&0x0F)<<28 | uint32(header[1])<<20 | uint32(header[2])<<12 | uint32(header[3])<<4 | uint32(header[4]>>4)
|
||||
flags := header[4] & 0x0F
|
||||
|
||||
if version != 3 {
|
||||
return nil, errors.New("unsupported version")
|
||||
}
|
||||
|
||||
config := &LicenseConfig{
|
||||
EndDate: time.Unix(int64(timestamp), 0),
|
||||
}
|
||||
|
||||
// 分离配置数据和签名
|
||||
configDataEnd := len(dataBytes) - 32
|
||||
configBytes := dataBytes[5:configDataEnd]
|
||||
signature := dataBytes[configDataEnd:]
|
||||
|
||||
// 获取HMAC密钥用于验证
|
||||
hmacKey, err := getHMACKeyFromRSAKey(publicKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get HMAC key: %w", err)
|
||||
}
|
||||
|
||||
// 验证HMAC签名
|
||||
if !verifyHMAC(configBytes, signature, hmacKey) {
|
||||
return nil, errors.New("signature verification failed")
|
||||
}
|
||||
|
||||
// 从极简格式反序列化配置
|
||||
fullConfig, err := ultraCompactUnmarshal(configBytes, flags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
// 合并时间戳
|
||||
fullConfig.EndDate = config.EndDate
|
||||
return fullConfig, nil
|
||||
}
|
||||
|
||||
// verifyHMAC 验证HMAC签名
|
||||
func verifyHMAC(data []byte, signature []byte, key []byte) bool {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write(data)
|
||||
expectedMAC := h.Sum(nil)
|
||||
return hmac.Equal(signature, expectedMAC)
|
||||
}
|
||||
|
||||
// SignLicenseData 使用RSA私钥对数据进行签名
|
||||
func SignLicenseData(data []byte, privateKeyPath string) ([]byte, error) {
|
||||
// 获取RSA私钥
|
||||
privateKey, err := getLicensePrivateKey(privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get private key: %w", err)
|
||||
}
|
||||
|
||||
// 使用现有的RSA签名方法(PSS模式,与许可证保持一致)
|
||||
signature, err := privateKey.SignData(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign data: %w", err)
|
||||
}
|
||||
|
||||
// 解码Base64签名
|
||||
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode signature: %w", err)
|
||||
}
|
||||
|
||||
return signatureBytes, nil
|
||||
}
|
||||
|
||||
// VerifyLicenseSignature 使用RSA公钥验证签名
|
||||
func VerifyLicenseSignature(data []byte, signature []byte, publicKeyPath string) error {
|
||||
// 获取RSA公钥
|
||||
publicKey, err := getLicensePublicKey(publicKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get public key: %w", err)
|
||||
}
|
||||
|
||||
// 编码签名为Base64(与SignData方法匹配)
|
||||
signatureBase64 := base64.StdEncoding.EncodeToString(signature)
|
||||
|
||||
// 使用现有的RSA验证方法
|
||||
err = publicKey.VerifySignature(data, signatureBase64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("signature verification failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLicensePrivateKey 获取许可证RSA私钥
|
||||
func getLicensePrivateKey(privateKeyPath string) (*RSAPrivateKey, error) {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(privateKeyPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("RSA私钥文件不存在,请先生成RSA密钥对: %s", privateKeyPath)
|
||||
}
|
||||
|
||||
// 加载私钥
|
||||
privateKey, err := LoadRSAPrivateKeyFromFile(privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RSA私钥文件损坏或格式错误: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// generateRandomString 生成随机字符串
|
||||
func generateRandomString(length int) (string, error) {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
bytes := make([]byte, length)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, b := range bytes {
|
||||
bytes[i] = chars[b%byte(len(chars))]
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// generateChecksum 生成校验码
|
||||
func generateChecksum(input string) string {
|
||||
sum := 0
|
||||
for _, char := range input {
|
||||
sum += int(char)
|
||||
}
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
checksum := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
checksum += string(chars[sum%len(chars)])
|
||||
sum = sum / len(chars)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
Reference in new issue
Block a user