update
This commit is contained in:
@@ -114,6 +114,39 @@ curl "http://localhost:8080/api/admin/miniapps?status=1&page=1&size=20" \
|
|||||||
{ "code": 0, "msg": "ok" }
|
{ "code": 0, "msg": "ok" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 1.6 `PUT /api/admin/miniapps/:id/status`
|
||||||
|
|
||||||
|
更新小程序状态(启用 / 停用)。
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | uint64 | 小程序主键。 |
|
||||||
|
|
||||||
|
**请求体**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "status": 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `status` | int8 | 是 | 状态:`0` 停用,`1` 启用。仅允许这两个值,非法值返回 `40000`。 |
|
||||||
|
|
||||||
|
> 该接口仅更新 `status` 字段,不影响其它字段;常用于停用 / 启用小程序。
|
||||||
|
|
||||||
|
**响应**:返回更新后的 `MiniProgram` 对象(见 §2.1),`app_secret` 字段不返回。
|
||||||
|
|
||||||
|
**curl 示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT "http://localhost:8080/api/admin/miniapps/1/status" \
|
||||||
|
-H "Authorization: Bearer <JWT>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"status":0}'
|
||||||
|
```
|
||||||
|
|
||||||
## 2. 数据模型
|
## 2. 数据模型
|
||||||
|
|
||||||
### 2.1 MiniProgram(被纳管小程序)
|
### 2.1 MiniProgram(被纳管小程序)
|
||||||
@@ -127,8 +160,8 @@ curl "http://localhost:8080/api/admin/miniapps?status=1&page=1&size=20" \
|
|||||||
| `AppSecret` | string | `app_secret` | **不返回**(json `-`)。 |
|
| `AppSecret` | string | `app_secret` | **不返回**(json `-`)。 |
|
||||||
| `Status` | int8 | `status` | `0` 停用,`1` 启用。 |
|
| `Status` | int8 | `status` | `0` 停用,`1` 启用。 |
|
||||||
| `Ext` | string | `ext` | 扩展 JSON 文本。 |
|
| `Ext` | string | `ext` | 扩展 JSON 文本。 |
|
||||||
| `CreatedAt` | time | `created_at` | 创建时间。 |
|
| `CreatedAt` | time | `created_at` | 创建时间,格式 `Y-m-d H:i:s`(如 `2026-10-26 15:04:05`)。 |
|
||||||
| `UpdatedAt` | time | `updated_at` | 更新时间。 |
|
| `UpdatedAt` | time | `updated_at` | 更新时间,格式 `Y-m-d H:i:s`。 |
|
||||||
|
|
||||||
## 3. 备注
|
## 3. 备注
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,25 @@ func (h *Handler) Delete(c *gin.Context) {
|
|||||||
response.OK(c, nil)
|
response.OK(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateStatus 更新小程序状态(0 禁用、1 启用)。
|
||||||
|
func (h *Handler) UpdateStatus(c *gin.Context) {
|
||||||
|
id, ok := parseID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req miniappsvc.StatusReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.Error(errors.ErrBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m, err := h.svc.UpdateStatus(c.Request.Context(), id, req.Status)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, m)
|
||||||
|
}
|
||||||
|
|
||||||
// parseID 从路径参数解析并校验 uint64 主键。
|
// parseID 从路径参数解析并校验 uint64 主键。
|
||||||
func parseID(c *gin.Context) (uint64, bool) {
|
func parseID(c *gin.Context) (uint64, bool) {
|
||||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
@@ -117,5 +136,6 @@ func Register(g *gin.RouterGroup, h *Handler) {
|
|||||||
rg.GET("/:id", middleware.RequirePerm(middleware.PermMiniView), h.Get)
|
rg.GET("/:id", middleware.RequirePerm(middleware.PermMiniView), h.Get)
|
||||||
rg.GET("", middleware.RequirePerm(middleware.PermMiniView), h.List)
|
rg.GET("", middleware.RequirePerm(middleware.PermMiniView), h.List)
|
||||||
rg.DELETE("/:id", middleware.RequirePerm(middleware.PermMiniManage), h.Delete)
|
rg.DELETE("/:id", middleware.RequirePerm(middleware.PermMiniManage), h.Delete)
|
||||||
|
rg.PUT("/:id/status", middleware.RequirePerm(middleware.PermMiniManage), h.UpdateStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"portal-mini-go/internal/model"
|
"portal-mini-go/internal/model"
|
||||||
"portal-mini-go/internal/pkg/errors"
|
"portal-mini-go/internal/pkg/errors"
|
||||||
"portal-mini-go/internal/pkg/response"
|
"portal-mini-go/internal/pkg/response"
|
||||||
|
"portal-mini-go/internal/pkg/types"
|
||||||
app1svc "portal-mini-go/internal/service/miniapp/app1"
|
app1svc "portal-mini-go/internal/service/miniapp/app1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,12 +111,12 @@ func (h *Handler) ListUsers(c *gin.Context) {
|
|||||||
|
|
||||||
// ProfileView 当前用户自身的资料视图(含自身 openid/unionid)。
|
// ProfileView 当前用户自身的资料视图(含自身 openid/unionid)。
|
||||||
type ProfileView struct {
|
type ProfileView struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
OpenID string `json:"openid"`
|
OpenID string `json:"openid"`
|
||||||
UnionID string `json:"union_id"`
|
UnionID string `json:"union_id"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt types.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicUserView 对外的用户公开视图,剔除 openid/unionid 等敏感字段。
|
// PublicUserView 对外的用户公开视图,剔除 openid/unionid 等敏感字段。
|
||||||
@@ -132,7 +133,7 @@ func toProfileView(u *model.User) ProfileView {
|
|||||||
UnionID: u.UnionID,
|
UnionID: u.UnionID,
|
||||||
Nickname: u.Nickname,
|
Nickname: u.Nickname,
|
||||||
Avatar: u.Avatar,
|
Avatar: u.Avatar,
|
||||||
CreatedAt: u.CreatedAt.Unix(),
|
CreatedAt: u.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
// 因此本包模型不进小程序数据隔离逻辑。
|
// 因此本包模型不进小程序数据隔离逻辑。
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
import "time"
|
import "portal-mini-go/internal/pkg/types"
|
||||||
|
|
||||||
// User 是后台管理员账号。用户名全局唯一,密码以 bcrypt 哈希存储。
|
// User 是后台管理员账号。用户名全局唯一,密码以 bcrypt 哈希存储。
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
Username string `gorm:"column:username;type:varchar(64);uniqueIndex;not null" json:"username"`
|
Username string `gorm:"column:username;type:varchar(64);uniqueIndex;not null" json:"username"`
|
||||||
PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"`
|
PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"`
|
||||||
Nickname string `gorm:"column:nickname;type:varchar(128)" json:"nickname"`
|
Nickname string `gorm:"column:nickname;type:varchar(128)" json:"nickname"`
|
||||||
Status int8 `gorm:"column:status;type:tinyint;not null;default:1" json:"status"`
|
Status int8 `gorm:"column:status;type:tinyint;not null;default:1" json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt types.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt types.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (User) TableName() string { return "admin_users" }
|
func (User) TableName() string { return "admin_users" }
|
||||||
@@ -26,13 +26,13 @@ const (
|
|||||||
|
|
||||||
// Role 是后台角色,与权限多对多关联,再与管理员多对多关联。
|
// Role 是后台角色,与权限多对多关联,再与管理员多对多关联。
|
||||||
type Role struct {
|
type Role struct {
|
||||||
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
Code string `gorm:"column:code;type:varchar(64);uniqueIndex;not null" json:"code"`
|
Code string `gorm:"column:code;type:varchar(64);uniqueIndex;not null" json:"code"`
|
||||||
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"`
|
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"`
|
||||||
Description string `gorm:"column:description;type:varchar(512)" json:"description"`
|
Description string `gorm:"column:description;type:varchar(512)" json:"description"`
|
||||||
Status int8 `gorm:"column:status;type:tinyint;not null;default:1" json:"status"`
|
Status int8 `gorm:"column:status;type:tinyint;not null;default:1" json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt types.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt types.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Role) TableName() string { return "admin_roles" }
|
func (Role) TableName() string { return "admin_roles" }
|
||||||
@@ -40,15 +40,15 @@ func (Role) TableName() string { return "admin_roles" }
|
|||||||
// Permission 是后台权限,分接口(api)与菜单(menu)两类。
|
// Permission 是后台权限,分接口(api)与菜单(menu)两类。
|
||||||
// 接口权限 code 对应后端 API 守卫;菜单权限 code 用于前端动态渲染菜单树。
|
// 接口权限 code 对应后端 API 守卫;菜单权限 code 用于前端动态渲染菜单树。
|
||||||
type Permission struct {
|
type Permission struct {
|
||||||
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
Code string `gorm:"column:code;type:varchar(128);uniqueIndex;not null" json:"code"`
|
Code string `gorm:"column:code;type:varchar(128);uniqueIndex;not null" json:"code"`
|
||||||
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"`
|
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"`
|
||||||
Type string `gorm:"column:type;type:varchar(16);not null;default:'api'" json:"type"`
|
Type string `gorm:"column:type;type:varchar(16);not null;default:'api'" json:"type"`
|
||||||
Method string `gorm:"column:method;type:varchar(16)" json:"method"`
|
Method string `gorm:"column:method;type:varchar(16)" json:"method"`
|
||||||
Path string `gorm:"column:path;type:varchar(255)" json:"path"`
|
Path string `gorm:"column:path;type:varchar(255)" json:"path"`
|
||||||
ParentID uint64 `gorm:"column:parent_id;index" json:"parent_id"`
|
ParentID uint64 `gorm:"column:parent_id;index" json:"parent_id"`
|
||||||
Sort int `gorm:"column:sort;not null;default:0" json:"sort"`
|
Sort int `gorm:"column:sort;not null;default:0" json:"sort"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt types.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Permission) TableName() string { return "admin_permissions" }
|
func (Permission) TableName() string { return "admin_permissions" }
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"portal-mini-go/internal/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MiniProgram 是被后台纳管的微信小程序配置。
|
// MiniProgram 是被后台纳管的微信小程序配置。
|
||||||
@@ -17,8 +17,8 @@ type MiniProgram struct {
|
|||||||
AppSecret string `gorm:"column:app_secret;type:varchar(128);not null" json:"-"`
|
AppSecret string `gorm:"column:app_secret;type:varchar(128);not null" json:"-"`
|
||||||
Status int8 `gorm:"type:tinyint;not null;default:1" json:"status"`
|
Status int8 `gorm:"type:tinyint;not null;default:1" json:"status"`
|
||||||
Ext string `gorm:"type:text" json:"ext"`
|
Ext string `gorm:"type:text" json:"ext"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt types.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt types.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -3,20 +3,20 @@
|
|||||||
// 各小程序用户互不关联(open_id 在不同 appid 下天然互不冲突,故无需额外隔离键)。
|
// 各小程序用户互不关联(open_id 在不同 appid 下天然互不冲突,故无需额外隔离键)。
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import "time"
|
import "portal-mini-go/internal/pkg/types"
|
||||||
|
|
||||||
// User 是小程序端用户。MiniID 关联被纳管的小程序(mini_programs.id),
|
// User 是小程序端用户。MiniID 关联被纳管的小程序(mini_programs.id),
|
||||||
// 与 open_id 共同唯一标识一个用户:(mini_id, open_id)。
|
// 与 open_id 共同唯一标识一个用户:(mini_id, open_id)。
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
MiniID uint64 `gorm:"column:mini_id;index:idx_mini_openid" json:"mini_id"`
|
MiniID uint64 `gorm:"column:mini_id;index:idx_mini_openid" json:"mini_id"`
|
||||||
OpenID string `gorm:"type:varchar(64);index:idx_mini_openid" json:"openid"`
|
OpenID string `gorm:"type:varchar(64);index:idx_mini_openid" json:"openid"`
|
||||||
UnionID string `gorm:"type:varchar(64);index" json:"union_id"`
|
UnionID string `gorm:"type:varchar(64);index" json:"union_id"`
|
||||||
Nickname string `gorm:"type:varchar(128)" json:"nickname"`
|
Nickname string `gorm:"type:varchar(128)" json:"nickname"`
|
||||||
Avatar string `gorm:"type:varchar(512)" json:"avatar"`
|
Avatar string `gorm:"type:varchar(512)" json:"avatar"`
|
||||||
Status int8 `gorm:"column:status;type:tinyint;not null;default:1" json:"status"`
|
Status int8 `gorm:"column:status;type:tinyint;not null;default:1" json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt types.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt types.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (User) TableName() string { return "users" }
|
func (User) TableName() string { return "users" }
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Package types 提供可复用的基础类型。
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeFormat 是时间字段返回给前端的标准格式(Y-m-d H:i:s)。
|
||||||
|
const TimeFormat = "2006-01-02 15:04:05"
|
||||||
|
|
||||||
|
// Time 是对 time.Time 的封装:JSON 序列化输出 TimeFormat 格式,
|
||||||
|
// 同时实现 driver.Valuer / sql.Scanner 以及与 GORM 的 GormDataTypeInterface,
|
||||||
|
// 使其既能作为模型字段被 GORM 自动维护(CreatedAt/UpdatedAt),又能以
|
||||||
|
// "2006-01-02 15:04:05" 字符串形式返回给前端。
|
||||||
|
type Time struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTime 基于 time.Time 构造 Time。
|
||||||
|
func NewTime(t time.Time) Time { return Time{Time: t} }
|
||||||
|
|
||||||
|
// Now 返回当前时间的 Time 封装。
|
||||||
|
func Now() Time { return Time{Time: time.Now()} }
|
||||||
|
|
||||||
|
// GormDataType 让 GORM 将该字段识别为时间类型,从而启用自动时间戳。
|
||||||
|
func (Time) GormDataType() schema.DataType { return schema.Time }
|
||||||
|
|
||||||
|
func (t Time) MarshalJSON() ([]byte, error) {
|
||||||
|
if t.Time.IsZero() {
|
||||||
|
return []byte(`""`), nil
|
||||||
|
}
|
||||||
|
return []byte(`"` + t.Time.Format(TimeFormat) + `"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON 解析前端传入的时间字符串,兼容 TimeFormat 与 RFC3339。
|
||||||
|
func (t *Time) UnmarshalJSON(b []byte) error {
|
||||||
|
s := strings.Trim(string(b), `"`)
|
||||||
|
if s == "" || s == "null" {
|
||||||
|
t.Time = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := time.ParseInLocation(TimeFormat, s, time.Local)
|
||||||
|
if err != nil {
|
||||||
|
if parsed, err = time.Parse(time.RFC3339, s); err != nil {
|
||||||
|
return fmt.Errorf("types.Time: 解析 %q 失败: %w", s, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Time = parsed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value 实现 driver.Valuer,写入数据库时返回底层 time.Time。
|
||||||
|
func (t Time) Value() (driver.Value, error) {
|
||||||
|
if t.Time.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return t.Time, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现 sql.Scanner,从数据库读取时还原为 time.Time。
|
||||||
|
func (t *Time) Scan(src interface{}) error {
|
||||||
|
if src == nil {
|
||||||
|
t.Time = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch v := src.(type) {
|
||||||
|
case time.Time:
|
||||||
|
t.Time = v
|
||||||
|
case *time.Time:
|
||||||
|
if v == nil {
|
||||||
|
t.Time = time.Time{}
|
||||||
|
} else {
|
||||||
|
t.Time = *v
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
parsed, err := time.ParseInLocation(TimeFormat, string(v), time.Local)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("types.Time: 扫描 []byte %q 失败: %w", string(v), err)
|
||||||
|
}
|
||||||
|
t.Time = parsed
|
||||||
|
case string:
|
||||||
|
parsed, err := time.ParseInLocation(TimeFormat, v, time.Local)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("types.Time: 扫描 string %q 失败: %w", v, err)
|
||||||
|
}
|
||||||
|
t.Time = parsed
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("types.Time: 不支持的扫描类型 %T", src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String 返回 TimeFormat 格式的字符串表示。
|
||||||
|
func (t Time) String() string {
|
||||||
|
if t.Time.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Time.Format(TimeFormat)
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTime_MarshalJSON(t *testing.T) {
|
||||||
|
t.Run("正常时间输出 Y-m-d H:i:s", func(t *testing.T) {
|
||||||
|
tm := NewTime(time.Date(2026, 10, 26, 15, 4, 5, 0, time.UTC))
|
||||||
|
b, err := json.Marshal(tm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.JSONEq(t, `"2026-10-26 15:04:05"`, string(b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("零值输出空字符串", func(t *testing.T) {
|
||||||
|
b, err := json.Marshal(Time{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.JSONEq(t, `""`, string(b))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTime_UnmarshalJSON(t *testing.T) {
|
||||||
|
t.Run("Y-m-d H:i:s", func(t *testing.T) {
|
||||||
|
var tm Time
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(`"2026-10-26 15:04:05"`), &tm))
|
||||||
|
assert.Equal(t, time.Date(2026, 10, 26, 15, 4, 5, 0, time.Local), tm.Time)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RFC3339 兼容", func(t *testing.T) {
|
||||||
|
var tm Time
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(`"2026-10-26T15:04:05Z"`), &tm))
|
||||||
|
assert.Equal(t, 2026, tm.Year())
|
||||||
|
assert.Equal(t, time.October, tm.Month())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("空字符串与 null", func(t *testing.T) {
|
||||||
|
for _, in := range []string{`""`, `null`} {
|
||||||
|
var tm Time
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(in), &tm))
|
||||||
|
assert.True(t, tm.IsZero())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("非法格式报错", func(t *testing.T) {
|
||||||
|
var tm Time
|
||||||
|
assert.Error(t, json.Unmarshal([]byte(`"not-a-time"`), &tm))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTime_Scan(t *testing.T) {
|
||||||
|
src := time.Date(2026, 10, 26, 15, 4, 5, 0, time.UTC)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in interface{}
|
||||||
|
want time.Time
|
||||||
|
}{
|
||||||
|
{"time.Time", src, src},
|
||||||
|
{"*time.Time", &src, src},
|
||||||
|
{"[]byte", []byte("2026-10-26 15:04:05"), time.Date(2026, 10, 26, 15, 4, 5, 0, time.Local)},
|
||||||
|
{"string", "2026-10-26 15:04:05", time.Date(2026, 10, 26, 15, 4, 5, 0, time.Local)},
|
||||||
|
{"nil", nil, time.Time{}},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
var tm Time
|
||||||
|
require.NoError(t, tm.Scan(c.in))
|
||||||
|
if c.want.IsZero() {
|
||||||
|
assert.True(t, tm.IsZero())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.True(t, c.want.Equal(tm.Time), "got %v want %v", tm.Time, c.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("不支持的类型报错", func(t *testing.T) {
|
||||||
|
var tm Time
|
||||||
|
assert.Error(t, tm.Scan(123))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTime_Value(t *testing.T) {
|
||||||
|
src := time.Date(2026, 10, 26, 15, 4, 5, 0, time.UTC)
|
||||||
|
v, err := NewTime(src).Value()
|
||||||
|
require.NoError(t, err)
|
||||||
|
got, ok := v.(time.Time)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.True(t, src.Equal(got))
|
||||||
|
|
||||||
|
// 零值返回 nil。
|
||||||
|
v, err = Time{}.Value()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTime_GormDataType(t *testing.T) {
|
||||||
|
assert.Equal(t, schema.Time, Time{}.GormDataType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 Time 满足相关接口,编译期校验。
|
||||||
|
var (
|
||||||
|
_ driver.Valuer = Time{}
|
||||||
|
_ interface{ Scan(interface{}) error } = (*Time)(nil)
|
||||||
|
_ json.Marshaler = Time{}
|
||||||
|
_ json.Unmarshaler = (*Time)(nil)
|
||||||
|
)
|
||||||
@@ -117,6 +117,19 @@ func (s *Service) Delete(ctx context.Context, id uint64) error {
|
|||||||
return s.repo.Delete(ctx, id)
|
return s.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StatusReq 更新小程序状态请求。状态仅允许 0(禁用)或 1(启用)。
|
||||||
|
type StatusReq struct {
|
||||||
|
Status int8 `json:"status" binding:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus 按 ID 更新小程序状态(0 禁用、1 启用),返回更新后的小程序。
|
||||||
|
func (s *Service) UpdateStatus(ctx context.Context, id uint64, status int8) (*model.MiniProgram, error) {
|
||||||
|
if err := s.repo.Update(ctx, id, map[string]interface{}{"status": status}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
// validateExt 校验扩展字段为合法 JSON(允许为空字符串)。
|
// validateExt 校验扩展字段为合法 JSON(允许为空字符串)。
|
||||||
func validateExt(s string) error {
|
func validateExt(s string) error {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user