f
This commit is contained in:
154
internal/domains/api/entities/api_call.go
Normal file
154
internal/domains/api/entities/api_call.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ApiCallStatus API调用状态
|
||||
const (
|
||||
ApiCallStatusPending = "pending"
|
||||
ApiCallStatusSuccess = "success"
|
||||
ApiCallStatusFailed = "failed"
|
||||
)
|
||||
|
||||
// ApiCall错误类型常量定义,供各领域服务和应用层统一引用。
|
||||
// 使用时可通过 entities.ApiCallErrorInvalidAccess 方式获得,编辑器可自动补全和提示。
|
||||
// 错误类型与业务含义:
|
||||
//
|
||||
// ApiCallErrorInvalidAccess = "invalid_access" // 无效AccessId
|
||||
// ApiCallErrorFrozenAccount = "frozen_account" // 账户冻结
|
||||
// ApiCallErrorInvalidIP = "invalid_ip" // IP无效
|
||||
// ApiCallErrorArrears = "arrears" // 账户欠费
|
||||
// ApiCallErrorNotSubscribed = "not_subscribed" // 未订阅产品
|
||||
// ApiCallErrorProductNotFound = "product_not_found" // 产品不存在
|
||||
// ApiCallErrorProductDisabled = "product_disabled" // 产品已停用
|
||||
// ApiCallErrorSystem = "system_error" // 系统错误
|
||||
// ApiCallErrorDatasource = "datasource_error" // 数据源异常
|
||||
// ApiCallErrorInvalidParam = "invalid_param" // 参数不正确
|
||||
// ApiCallErrorDecryptFail = "decrypt_fail" // 解密失败
|
||||
const (
|
||||
ApiCallErrorInvalidAccess = "invalid_access" // 无效AccessId
|
||||
ApiCallErrorFrozenAccount = "frozen_account" // 账户冻结
|
||||
ApiCallErrorInvalidIP = "invalid_ip" // IP无效
|
||||
ApiCallErrorArrears = "arrears" // 账户欠费
|
||||
ApiCallErrorNotSubscribed = "not_subscribed" // 未订阅产品
|
||||
ApiCallErrorProductNotFound = "product_not_found" // 产品不存在
|
||||
ApiCallErrorProductDisabled = "product_disabled" // 产品已停用
|
||||
ApiCallErrorSystem = "system_error" // 系统错误
|
||||
ApiCallErrorDatasource = "datasource_error" // 数据源异常
|
||||
ApiCallErrorInvalidParam = "invalid_param" // 参数不正确
|
||||
ApiCallErrorDecryptFail = "decrypt_fail" // 解密失败
|
||||
ApiCallErrorQueryEmpty = "query_empty" // 查询为空
|
||||
)
|
||||
|
||||
// ApiCall API调用(聚合根)
|
||||
type ApiCall struct {
|
||||
ID string `gorm:"type:varchar(64);primaryKey" json:"id"`
|
||||
AccessId string `gorm:"type:varchar(64);not null;index" json:"access_id"`
|
||||
UserId *string `gorm:"type:varchar(36);index" json:"user_id,omitempty"`
|
||||
ProductId *string `gorm:"type:varchar(64);index" json:"product_id,omitempty"`
|
||||
TransactionId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"transaction_id"`
|
||||
ClientIp string `gorm:"type:varchar(64);not null;index" json:"client_ip"`
|
||||
RequestParams string `gorm:"type:text" json:"request_params"`
|
||||
ResponseData *string `gorm:"type:text" json:"response_data,omitempty"`
|
||||
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"`
|
||||
StartAt time.Time `gorm:"not null;index" json:"start_at"`
|
||||
EndAt *time.Time `gorm:"index" json:"end_at,omitempty"`
|
||||
Cost *decimal.Decimal `gorm:"default:0" json:"cost,omitempty"`
|
||||
ErrorType *string `gorm:"type:varchar(32)" json:"error_type,omitempty"`
|
||||
ErrorMsg *string `gorm:"type:varchar(256)" json:"error_msg,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
// NewApiCall 工厂方法
|
||||
func NewApiCall(accessId, requestParams, clientIp string) (*ApiCall, error) {
|
||||
if accessId == "" {
|
||||
return nil, errors.New("AccessId不能为空")
|
||||
}
|
||||
if clientIp == "" {
|
||||
return nil, errors.New("ClientIp不能为空")
|
||||
}
|
||||
|
||||
return &ApiCall{
|
||||
ID: uuid.New().String(),
|
||||
AccessId: accessId,
|
||||
TransactionId: GenerateTransactionID(),
|
||||
ClientIp: clientIp,
|
||||
RequestParams: requestParams,
|
||||
Status: ApiCallStatusPending,
|
||||
StartAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MarkSuccess 标记为成功
|
||||
func (a *ApiCall) MarkSuccess(cost decimal.Decimal) error {
|
||||
// 校验除ErrorMsg和ErrorType外所有字段不能为空
|
||||
if a.ID == "" || a.AccessId == "" || a.TransactionId == "" || a.Status == "" || a.StartAt.IsZero() {
|
||||
return errors.New("ApiCall字段不能为空(除ErrorMsg和ErrorType)")
|
||||
}
|
||||
// 可选字段也要有值
|
||||
if a.UserId == nil || a.ProductId == nil {
|
||||
return errors.New("ApiCall标记成功时UserId、ProductId不能为空")
|
||||
}
|
||||
a.Status = ApiCallStatusSuccess
|
||||
endAt := time.Now()
|
||||
a.EndAt = &endAt
|
||||
a.Cost = &cost
|
||||
a.ErrorType = nil
|
||||
a.ErrorMsg = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkFailed 标记为失败
|
||||
func (a *ApiCall) MarkFailed(errorType, errorMsg string) {
|
||||
a.Status = ApiCallStatusFailed
|
||||
a.ErrorType = &errorType
|
||||
shortMsg := errorMsg
|
||||
if len(shortMsg) > 120 {
|
||||
shortMsg = shortMsg[:120]
|
||||
}
|
||||
a.ErrorMsg = &shortMsg
|
||||
endAt := time.Now()
|
||||
a.EndAt = &endAt
|
||||
}
|
||||
|
||||
// Validate 校验ApiCall聚合根的业务规则
|
||||
func (a *ApiCall) Validate() error {
|
||||
if a.ID == "" {
|
||||
return errors.New("ID不能为空")
|
||||
}
|
||||
if a.AccessId == "" {
|
||||
return errors.New("AccessId不能为空")
|
||||
}
|
||||
if a.TransactionId == "" {
|
||||
return errors.New("TransactionId不能为空")
|
||||
}
|
||||
if a.Status != ApiCallStatusPending && a.Status != ApiCallStatusSuccess && a.Status != ApiCallStatusFailed {
|
||||
return errors.New("无效的调用状态")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateTransactionID 生成UUID格式的交易单号
|
||||
func GenerateTransactionID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (ApiCall) TableName() string {
|
||||
return "api_calls"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (c *ApiCall) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == "" {
|
||||
c.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
362
internal/domains/api/entities/api_user.go
Normal file
362
internal/domains/api/entities/api_user.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ApiUserStatus API用户状态
|
||||
const (
|
||||
ApiUserStatusNormal = "normal"
|
||||
ApiUserStatusFrozen = "frozen"
|
||||
)
|
||||
|
||||
// WhiteListItem 白名单项,包含IP地址、添加时间和备注
|
||||
type WhiteListItem struct {
|
||||
IPAddress string `json:"ip_address"` // IP地址
|
||||
AddedAt time.Time `json:"added_at"` // 添加时间
|
||||
Remark string `json:"remark"` // 备注
|
||||
}
|
||||
|
||||
// WhiteList 白名单类型,支持向后兼容(旧的字符串数组格式)
|
||||
type WhiteList []WhiteListItem
|
||||
|
||||
// Value 实现 driver.Valuer 接口,用于数据库写入
|
||||
func (w WhiteList) Value() (driver.Value, error) {
|
||||
if w == nil {
|
||||
return "[]", nil
|
||||
}
|
||||
data, err := json.Marshal(w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Scan 实现 sql.Scanner 接口,用于数据库读取(支持向后兼容)
|
||||
func (w *WhiteList) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*w = WhiteList{}
|
||||
return nil
|
||||
}
|
||||
|
||||
var bytes []byte
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
bytes = v
|
||||
case string:
|
||||
bytes = []byte(v)
|
||||
default:
|
||||
return errors.New("无法扫描 WhiteList 类型")
|
||||
}
|
||||
|
||||
if len(bytes) == 0 || string(bytes) == "[]" || string(bytes) == "null" {
|
||||
*w = WhiteList{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 首先尝试解析为新格式(结构体数组)
|
||||
var items []WhiteListItem
|
||||
if err := json.Unmarshal(bytes, &items); err == nil {
|
||||
// 成功解析为新格式
|
||||
*w = WhiteList(items)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果失败,尝试解析为旧格式(字符串数组)
|
||||
var oldFormat []string
|
||||
if err := json.Unmarshal(bytes, &oldFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 将旧格式转换为新格式
|
||||
now := time.Now()
|
||||
items = make([]WhiteListItem, 0, len(oldFormat))
|
||||
for _, ip := range oldFormat {
|
||||
items = append(items, WhiteListItem{
|
||||
IPAddress: ip,
|
||||
AddedAt: now, // 使用当前时间作为添加时间(因为旧数据没有时间信息)
|
||||
Remark: "", // 旧数据没有备注信息
|
||||
})
|
||||
}
|
||||
*w = WhiteList(items)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApiUser API用户(聚合根)
|
||||
type ApiUser struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
|
||||
UserId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"user_id"`
|
||||
AccessId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"access_id"`
|
||||
SecretKey string `gorm:"type:varchar(128);not null" json:"secret_key"`
|
||||
Status string `gorm:"type:varchar(20);not null;default:'normal'" json:"status"`
|
||||
WhiteList WhiteList `gorm:"type:json;default:'[]'" json:"white_list"` // 支持多个白名单,包含IP和添加时间,支持向后兼容
|
||||
|
||||
// 余额预警配置
|
||||
BalanceAlertEnabled bool `gorm:"default:true" json:"balance_alert_enabled" comment:"是否启用余额预警"`
|
||||
BalanceAlertThreshold float64 `gorm:"default:200.00" json:"balance_alert_threshold" comment:"余额预警阈值"`
|
||||
AlertPhone string `gorm:"type:varchar(20)" json:"alert_phone" comment:"预警手机号"`
|
||||
LastLowBalanceAlert *time.Time `json:"last_low_balance_alert" comment:"最后低余额预警时间"`
|
||||
LastArrearsAlert *time.Time `json:"last_arrears_alert" comment:"最后欠费预警时间"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
// IsWhiteListed 校验IP/域名是否在白名单
|
||||
func (u *ApiUser) IsWhiteListed(target string) bool {
|
||||
for _, w := range u.WhiteList {
|
||||
if w.IPAddress == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsActive 是否可用
|
||||
func (u *ApiUser) IsActive() bool {
|
||||
return u.Status == ApiUserStatusNormal
|
||||
}
|
||||
|
||||
// IsFrozen 是否冻结
|
||||
func (u *ApiUser) IsFrozen() bool {
|
||||
return u.Status == ApiUserStatusFrozen
|
||||
}
|
||||
|
||||
// NewApiUser 工厂方法
|
||||
func NewApiUser(userId string, defaultAlertEnabled bool, defaultAlertThreshold float64) (*ApiUser, error) {
|
||||
if userId == "" {
|
||||
return nil, errors.New("用户ID不能为空")
|
||||
}
|
||||
accessId, err := GenerateSecretId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secretKey, err := GenerateSecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ApiUser{
|
||||
ID: uuid.New().String(),
|
||||
UserId: userId,
|
||||
AccessId: accessId,
|
||||
SecretKey: secretKey,
|
||||
Status: ApiUserStatusNormal,
|
||||
WhiteList: WhiteList{},
|
||||
BalanceAlertEnabled: defaultAlertEnabled,
|
||||
BalanceAlertThreshold: defaultAlertThreshold,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 领域行为
|
||||
func (u *ApiUser) Freeze() {
|
||||
u.Status = ApiUserStatusFrozen
|
||||
}
|
||||
func (u *ApiUser) Unfreeze() {
|
||||
u.Status = ApiUserStatusNormal
|
||||
}
|
||||
func (u *ApiUser) UpdateWhiteList(list []WhiteListItem) {
|
||||
u.WhiteList = WhiteList(list)
|
||||
}
|
||||
|
||||
// AddToWhiteList 新增白名单项(防御性校验)
|
||||
func (u *ApiUser) AddToWhiteList(entry string, remark string) error {
|
||||
if len(u.WhiteList) >= 10 {
|
||||
return errors.New("白名单最多只能有10个")
|
||||
}
|
||||
if net.ParseIP(entry) == nil {
|
||||
return errors.New("非法IP")
|
||||
}
|
||||
for _, w := range u.WhiteList {
|
||||
if w.IPAddress == entry {
|
||||
return errors.New("白名单已存在")
|
||||
}
|
||||
}
|
||||
u.WhiteList = append(u.WhiteList, WhiteListItem{
|
||||
IPAddress: entry,
|
||||
AddedAt: time.Now(),
|
||||
Remark: remark,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate GORM钩子:更新前确保WhiteList不为nil
|
||||
func (u *ApiUser) BeforeUpdate(tx *gorm.DB) error {
|
||||
if u.WhiteList == nil {
|
||||
u.WhiteList = WhiteList{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveFromWhiteList 删除白名单项
|
||||
func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
||||
newList := make([]WhiteListItem, 0, len(u.WhiteList))
|
||||
for _, w := range u.WhiteList {
|
||||
if w.IPAddress != entry {
|
||||
newList = append(newList, w)
|
||||
}
|
||||
}
|
||||
if len(newList) == len(u.WhiteList) {
|
||||
return errors.New("白名单不存在")
|
||||
}
|
||||
u.WhiteList = newList
|
||||
return nil
|
||||
}
|
||||
|
||||
// 余额预警相关方法
|
||||
|
||||
// UpdateBalanceAlertSettings 更新余额预警设置
|
||||
func (u *ApiUser) UpdateBalanceAlertSettings(enabled bool, threshold float64, phone string) error {
|
||||
if threshold < 0 {
|
||||
return errors.New("预警阈值不能为负数")
|
||||
}
|
||||
if phone != "" && len(phone) != 11 {
|
||||
return errors.New("手机号格式不正确")
|
||||
}
|
||||
|
||||
u.BalanceAlertEnabled = enabled
|
||||
u.BalanceAlertThreshold = threshold
|
||||
u.AlertPhone = phone
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldSendLowBalanceAlert 是否应该发送低余额预警(24小时冷却期)
|
||||
func (u *ApiUser) ShouldSendLowBalanceAlert(balance float64) bool {
|
||||
if !u.BalanceAlertEnabled || u.AlertPhone == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 余额低于阈值
|
||||
if balance < u.BalanceAlertThreshold {
|
||||
// 检查是否已经发送过预警(避免频繁发送)
|
||||
if u.LastLowBalanceAlert != nil {
|
||||
// 如果距离上次预警不足24小时,不发送
|
||||
if time.Since(*u.LastLowBalanceAlert) < 24*time.Hour {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ShouldSendArrearsAlert 是否应该发送欠费预警(不受冷却期限制)
|
||||
func (u *ApiUser) ShouldSendArrearsAlert(balance float64) bool {
|
||||
if !u.BalanceAlertEnabled || u.AlertPhone == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 余额为负数(欠费)- 欠费预警不受冷却期限制
|
||||
if balance < 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarkLowBalanceAlertSent 标记低余额预警已发送
|
||||
func (u *ApiUser) MarkLowBalanceAlertSent() {
|
||||
now := time.Now()
|
||||
u.LastLowBalanceAlert = &now
|
||||
}
|
||||
|
||||
// MarkArrearsAlertSent 标记欠费预警已发送
|
||||
func (u *ApiUser) MarkArrearsAlertSent() {
|
||||
now := time.Now()
|
||||
u.LastArrearsAlert = &now
|
||||
}
|
||||
|
||||
// Validate 校验ApiUser聚合根的业务规则
|
||||
func (u *ApiUser) Validate() error {
|
||||
if u.UserId == "" {
|
||||
return errors.New("用户ID不能为空")
|
||||
}
|
||||
if u.AccessId == "" {
|
||||
return errors.New("AccessId不能为空")
|
||||
}
|
||||
if u.SecretKey == "" {
|
||||
return errors.New("SecretKey不能为空")
|
||||
}
|
||||
switch u.Status {
|
||||
case ApiUserStatusNormal, ApiUserStatusFrozen:
|
||||
// ok
|
||||
default:
|
||||
return errors.New("无效的用户状态")
|
||||
}
|
||||
if len(u.WhiteList) > 10 {
|
||||
return errors.New("白名单最多只能有10个")
|
||||
}
|
||||
for _, item := range u.WhiteList {
|
||||
if net.ParseIP(item.IPAddress) == nil {
|
||||
return errors.New("白名单项必须为合法IP地址: " + item.IPAddress)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 生成AES-128密钥的函数,符合市面规范
|
||||
func GenerateSecretKey() (string, error) {
|
||||
key := make([]byte, 16) // 16字节密钥
|
||||
_, err := io.ReadFull(rand.Reader, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
func GenerateSecretId() (string, error) {
|
||||
// 创建一个字节数组,用于存储随机数据
|
||||
bytes := make([]byte, 8) // 因为每个字节表示两个16进制字符
|
||||
|
||||
// 读取随机字节到数组中
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 将字节数组转换为16进制字符串
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (ApiUser) TableName() string {
|
||||
return "api_users"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID并确保WhiteList不为nil
|
||||
func (c *ApiUser) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == "" {
|
||||
c.ID = uuid.New().String()
|
||||
}
|
||||
if c.WhiteList == nil {
|
||||
c.WhiteList = WhiteList{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterFind GORM钩子:查询后处理数据,确保AddedAt不为零值
|
||||
func (u *ApiUser) AfterFind(tx *gorm.DB) error {
|
||||
// 如果 WhiteList 为空,初始化为空数组
|
||||
if u.WhiteList == nil {
|
||||
u.WhiteList = WhiteList{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 确保所有项的AddedAt不为零值(处理可能从旧数据迁移的情况)
|
||||
now := time.Now()
|
||||
for i := range u.WhiteList {
|
||||
if u.WhiteList[i].AddedAt.IsZero() {
|
||||
u.WhiteList[i].AddedAt = now
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
35
internal/domains/api/entities/enterprise_report.go
Normal file
35
internal/domains/api/entities/enterprise_report.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
// Report 报告记录实体
|
||||
// 用于持久化存储各类报告(企业报告等),通过编号和类型区分
|
||||
type Report struct {
|
||||
// 报告编号,直接使用业务生成的 reportId 作为主键
|
||||
ReportID string `gorm:"primaryKey;type:varchar(64)" json:"report_id"`
|
||||
|
||||
// 报告类型,例如 enterprise(企业报告)、personal 等
|
||||
Type string `gorm:"type:varchar(32);not null;index" json:"type"`
|
||||
|
||||
// 调用来源API编码,例如 QYGLJ1U9
|
||||
ApiCode string `gorm:"type:varchar(32);not null;index" json:"api_code"`
|
||||
|
||||
// 企业名称和统一社会信用代码,便于后续检索
|
||||
EntName string `gorm:"type:varchar(255);index" json:"ent_name"`
|
||||
EntCode string `gorm:"type:varchar(64);index" json:"ent_code"`
|
||||
|
||||
// 原始请求参数(JSON字符串),用于审计和排错
|
||||
RequestParams string `gorm:"type:text" json:"request_params"`
|
||||
|
||||
// 报告完整JSON内容
|
||||
ReportData string `gorm:"type:text" json:"report_data"`
|
||||
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (Report) TableName() string {
|
||||
return "reports"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user