1
This commit is contained in:
+2
-1
@@ -40,7 +40,7 @@ Flutter App ── POST /auth/register|login ─► 账号注册/登录,签发
|
||||
| 表 | 说明 | 关键字段 |
|
||||
|---|---|---|
|
||||
| `payment_order` | 支付订单 | `order_id`(PK)、`phone_num`、`plan_id`、`channel`(wechat/alipay)、`amount_cents`、`status`(created/paid/closed)、`wx_trade_no`(UNIQUE)、`alipay_trade_no`(UNIQUE)、`created_at`、`paid_at` |
|
||||
| `license` | 手机号账号与授权 | `phone_num`(PK)、`password`(bcrypt)、`expires_at`(未充值 NULL)、`created_at`、`updated_at` |
|
||||
| `license` | 手机号账号与授权 | `phone_num`(PK)、`password`(bcrypt)、`expires_at`(未充值 NULL)、`remark`(管理端备注)、`created_at`、`updated_at` |
|
||||
|
||||
建表与迁移见 `技术设计.md`(新库直接建表;存量库以 `PRAGMA user_version` 版本化迁移)。
|
||||
|
||||
@@ -139,6 +139,7 @@ Flutter App ── POST /auth/register|login ─► 账号注册/登录,签发
|
||||
| GET | `/admin/licenses` | 账号/授权列表,筛选 `phoneNum` + `page/size`(含未充值账号) |
|
||||
| POST | `/admin/licenses/grant` | 手动授权 `{"phoneNum":"13800000000","planId":"day"}`,自然日叠加 |
|
||||
| POST | `/admin/licenses/revoke` | 撤销授权 `{"phoneNum":"13800000000"}`(清授权保留账号) |
|
||||
| POST | `/admin/licenses/remark` | 写账号备注 `{"phoneNum":"13800000000","remark":"..."}`(空串清空,≤200 字) |
|
||||
|
||||
管理页面(订单/授权)由 `server_admin/` 构建产物提供,访问 `http://<host>/admin/`。金额均为整数分,前端展示 ÷100 转元。
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>视野管理端</title>
|
||||
<script type="module" crossorigin src="/admin/assets/index-gy5ysx_E.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-DsTNKZnX.css">
|
||||
<script type="module" crossorigin src="/admin/assets/index-B_oC4njo.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-BcJw2rv4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -33,3 +33,8 @@ func (c *cAdmin) Grant(ctx context.Context, req *dto.AdminGrantReq) (*dto.AdminG
|
||||
func (c *cAdmin) Revoke(ctx context.Context, req *dto.AdminRevokeReq) (*dto.AdminRevokeRes, error) {
|
||||
return service.License.AdminRevoke(ctx, req)
|
||||
}
|
||||
|
||||
// Remark 写账号备注
|
||||
func (c *cAdmin) Remark(ctx context.Context, req *dto.AdminRemarkReq) (*dto.AdminRemarkRes, error) {
|
||||
return service.License.AdminRemark(ctx, req)
|
||||
}
|
||||
|
||||
@@ -24,12 +24,14 @@ func init() {
|
||||
phone_num TEXT PRIMARY KEY,
|
||||
password TEXT NOT NULL,
|
||||
expires_at TEXT,
|
||||
remark TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
common.EnsureColumn(ctx, consts.TableLicense, "remark", "remark TEXT")
|
||||
}
|
||||
|
||||
// GetByPhone 按手机号查询(走缓存,键含手机号)
|
||||
@@ -108,6 +110,15 @@ func (d *licenseDao) ClearAuthInTx(ctx context.Context, tx gdb.TX, phone string)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetRemarkInTx 事务内写备注(管理端 remark,空串即清空):不覆盖密码/授权字段
|
||||
func (d *licenseDao) SetRemarkInTx(ctx context.Context, tx gdb.TX, phone, remark string) error {
|
||||
_, err := g.DB().Model(consts.TableLicense).Ctx(ctx).TX(tx).
|
||||
Where("phone_num", phone).
|
||||
Data(gdb.Map{"remark": remark, "updated_at": gtime.Now()}).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// PageByPhone 管理端账号分页查询:phone 模糊筛选,按更新时间倒序;
|
||||
// 列表不缓存(单点 GetByPhone 仍走缓存,写后清缓存不变)。
|
||||
func (d *licenseDao) PageByPhone(ctx context.Context, phone string, page, size int) ([]*entity.License, int64, error) {
|
||||
|
||||
@@ -48,10 +48,11 @@ type AdminLicenseListReq struct {
|
||||
Size int `json:"size" v:"integer|min:1|max:100" dc:"每页条数,默认 20"`
|
||||
}
|
||||
|
||||
// AdminLicenseItem 账号条目(未充值时 expiresAt 为 null)
|
||||
// AdminLicenseItem 账号条目(未充值时 expiresAt 为 null;remark 为管理端备注)
|
||||
type AdminLicenseItem struct {
|
||||
PhoneNum string `json:"phoneNum"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||
}
|
||||
@@ -79,3 +80,12 @@ type AdminRevokeReq struct {
|
||||
}
|
||||
|
||||
type AdminRevokeRes struct{}
|
||||
|
||||
// AdminRemarkReq 写账号备注(空串即清空;仅管理端可见,客户端接口不含该字段)
|
||||
type AdminRemarkReq struct {
|
||||
g.Meta `path:"/licenses/remark" method:"post" summary:"写账号备注" tags:"管理端"`
|
||||
PhoneNum string `json:"phoneNum" v:"required|length:6,20" dc:"手机号"`
|
||||
Remark string `json:"remark" v:"length:0,200" dc:"备注内容,空串清空"`
|
||||
}
|
||||
|
||||
type AdminRemarkRes struct{}
|
||||
|
||||
@@ -8,6 +8,7 @@ type License struct {
|
||||
PhoneNum string `json:"phoneNum" orm:"phone_num" description:"手机号账号(主键)"`
|
||||
Password string `json:"-" orm:"password" description:"bcrypt 加盐哈希"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" orm:"expires_at" description:"到期时间(自然日,服务端时区),未充值 NULL"`
|
||||
Remark string `json:"remark" orm:"remark" description:"管理端备注(客户端不可见)"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"注册时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ func (s *licenseService) AdminListLicenses(ctx context.Context, req *dto.AdminLi
|
||||
for _, lic := range list {
|
||||
items = append(items, &dto.AdminLicenseItem{
|
||||
PhoneNum: lic.PhoneNum,
|
||||
ExpiresAt: lic.ExpiresAt, CreatedAt: lic.CreatedAt, UpdatedAt: lic.UpdatedAt,
|
||||
ExpiresAt: lic.ExpiresAt, Remark: lic.Remark,
|
||||
CreatedAt: lic.CreatedAt, UpdatedAt: lic.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return &dto.AdminLicenseListRes{Total: total, List: items}, nil
|
||||
@@ -102,3 +103,18 @@ func (s *licenseService) AdminRevoke(ctx context.Context, req *dto.AdminRevokeRe
|
||||
common.ClearCache(ctx, "license:"+req.PhoneNum)
|
||||
return &dto.AdminRevokeRes{}, nil
|
||||
}
|
||||
|
||||
// AdminRemark 写账号备注(管理端运营记录):空串即清空,不覆盖密码/授权;
|
||||
// 单写者串行事务内更新,提交后清授权缓存(列表不缓存不受影响)。
|
||||
func (s *licenseService) AdminRemark(ctx context.Context, req *dto.AdminRemarkReq) (*dto.AdminRemarkRes, error) {
|
||||
err := common.Serial().Submit(ctx, func() error {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
return dao.License.SetRemarkInTx(ctx, tx, req.PhoneNum, req.Remark)
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
common.ClearCache(ctx, "license:"+req.PhoneNum)
|
||||
return &dto.AdminRemarkRes{}, nil
|
||||
}
|
||||
|
||||
@@ -52,6 +52,24 @@ func ClearCache(ctx context.Context, keys ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureColumn 存量库迁移:列缺失时 ALTER TABLE ADD COLUMN(SQLite 支持表尾追加)。
|
||||
// 新库由 CREATE TABLE 直接含列、已迁移库列已存在,均跳过;失败 panic(启动即暴露)。
|
||||
func EnsureColumn(ctx context.Context, table, column, ddl string) {
|
||||
res, err := g.DB().GetAll(ctx, "PRAGMA table_info("+table+")")
|
||||
if err != nil {
|
||||
panic("检查表结构失败 " + table + ": " + err.Error())
|
||||
}
|
||||
for _, r := range res {
|
||||
if gconv.String(r["name"]) == column {
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+table+" ADD COLUMN "+ddl); err != nil {
|
||||
panic("迁移加列失败 " + table + "." + column + ": " + err.Error())
|
||||
}
|
||||
g.Log().Warningf(ctx, "存量表 %s 已迁移:新增列 %s", table, column)
|
||||
}
|
||||
|
||||
// DropLegacyTableIfHasColumn 存量库迁移:表存在旧版本废弃列(账号体系上线前的 device_id)
|
||||
// 时 DROP 整表(存量数据作废,用户决策),由 dao init 以新结构重建。
|
||||
func DropLegacyTableIfHasColumn(ctx context.Context, table, column string) {
|
||||
|
||||
@@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS license (
|
||||
phone_num TEXT PRIMARY KEY, -- 手机号账号,一账号一授权记录,续费更新
|
||||
password TEXT NOT NULL, -- bcrypt 加盐哈希,不存明文
|
||||
expires_at TEXT, -- 未充值 NULL(NULL/过期即未授权)
|
||||
remark TEXT, -- 管理端备注(运营发卡/人工记录,客户端不可见)
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
@@ -59,6 +60,7 @@ CREATE INDEX IF NOT EXISTS idx_payment_order_phone ON payment_order(phone_num);
|
||||
- v1 = 建表(各 dao init)+ 种子套餐(v1 已移除)
|
||||
- v2 = 删除 `license.plan_id` 列(该字段无业务语义,只留到期时间;`PRAGMA table_info` 检测列存在才 `ALTER TABLE ... DROP COLUMN`,新库与已迁移库跳过)
|
||||
- v3 = `DROP TABLE IF EXISTS plan`(套餐改配置后清残留表,订单快照 `payment_order.plan_id` 不受影响)
|
||||
- v4 = `license` 加 `remark` 列(管理端备注;`PRAGMA table_info` 检测列缺失才 `ALTER TABLE ... ADD COLUMN remark TEXT`,新库直接建表跳过)
|
||||
|
||||
## 账号体系(注册/登录)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user