You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
231 lines
7.9 KiB
231 lines
7.9 KiB
package console
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/lego/sys/mysql"
|
|
"yunyan/pb"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ============================ 终端用户域 (user) ============================
|
|
//
|
|
// 与设备域(console 主库)不同:终端用户住在【所选应用的业务库】里,所以这里的每个接口
|
|
// 都要先从 X-App-Id 解出应用、拿到该应用的业务库连接,再查它的 user 表。
|
|
//
|
|
// 原来只有「按 UID 精确查一个人」,运营手上没有 UID 就什么也做不了。
|
|
// 现在补了列表(分页+条件筛选)与参数修改。
|
|
|
|
// userListPageMax 单页上限。列表带 COUNT(*),页太大在几十万用户的库上会很慢;
|
|
// 真要导出应该另开导出接口,而不是把 page_size 调到几千。
|
|
const userListPageMax = 200
|
|
|
|
// buildUserListWhere 把筛选条件拼成 where 子句。
|
|
//
|
|
// keyword 一栏多用:UID 精确、其余字段模糊。做成一栏是因为运营手上拿到的线索形态不定
|
|
// (客服转来的可能是手机号、可能是邮箱、也可能是用户自己报的昵称),
|
|
// 让人先选「按什么查」再输入只是多一步。
|
|
func buildUserListWhere(req *userListReq, now int64) (string, []interface{}) {
|
|
conds := make([]string, 0, 6)
|
|
args := make([]interface{}, 0, 8)
|
|
if kw := strings.TrimSpace(req.Keyword); kw != "" {
|
|
like := "%" + kw + "%"
|
|
conds = append(conds, "(uid = ? OR phone LIKE ? OR mail LIKE ? OR name LIKE ? OR account LIKE ? OR phonemac LIKE ?)")
|
|
args = append(args, kw, like, like, like, like, like)
|
|
}
|
|
if req.State >= 0 {
|
|
conds = append(conds, "`State` = ?")
|
|
args = append(args, req.State)
|
|
}
|
|
// vip: 1=会员中(未过期) 0=非会员。按到期时间判定而不是 viplv——
|
|
// viplv 是等级,过期后并不会被清零,拿它判会把过期用户也算成会员。
|
|
switch req.Vip {
|
|
case 1:
|
|
conds = append(conds, "vipexptime > ?")
|
|
args = append(args, now)
|
|
case 0:
|
|
conds = append(conds, "(vipexptime IS NULL OR vipexptime <= ?)")
|
|
args = append(args, now)
|
|
}
|
|
if req.Idverified >= 0 {
|
|
conds = append(conds, "idverified = ?")
|
|
args = append(args, req.Idverified == 1)
|
|
}
|
|
if req.Start > 0 {
|
|
conds = append(conds, "createtime >= ?")
|
|
args = append(args, req.Start)
|
|
}
|
|
if req.End > 0 {
|
|
conds = append(conds, "createtime < ?")
|
|
args = append(args, req.End)
|
|
}
|
|
if len(conds) == 0 {
|
|
return "1=1", args
|
|
}
|
|
return strings.Join(conds, " AND "), args
|
|
}
|
|
|
|
type userListReq struct {
|
|
Keyword string `json:"keyword"`
|
|
State int32 `json:"state"` // -1 不限;0 正常 1 禁用 2 已注销
|
|
Vip int32 `json:"vip"` // -1 不限;1 会员中 0 非会员
|
|
Idverified int32 `json:"idverified"` // -1 不限;1 已实名 0 未实名
|
|
Start int64 `json:"start"` // 注册时间区间 [start, end)
|
|
End int64 `json:"end"`
|
|
Page int `json:"page"` // 从 0 起
|
|
PageSize int `json:"page_size"` // 默认 20,上限 userListPageMax
|
|
}
|
|
|
|
// getUsers 终端用户列表:分页 + 条件筛选。
|
|
func (this *serverComp) getUsers(c *gin.Context) {
|
|
appId := parseAppId(c.GetHeader("X-App-Id"))
|
|
if !this.requireAppScope(c, appId) {
|
|
return
|
|
}
|
|
conn, err := this.module.registry.getServiceDB(appId)
|
|
if err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
// 三个「不限」标志位默认必须是 -1 而不是 0:0 是合法取值(正常状态 / 非会员 / 未实名),
|
|
// 拿零值当「不限」会让不传参的默认查询悄悄只返回一部分人。
|
|
req := &userListReq{State: -1, Vip: -1, Idverified: -1}
|
|
_ = c.ShouldBindJSON(req)
|
|
if req.PageSize <= 0 {
|
|
req.PageSize = 20
|
|
}
|
|
if req.PageSize > userListPageMax {
|
|
req.PageSize = userListPageMax
|
|
}
|
|
if req.Page < 0 {
|
|
req.Page = 0
|
|
}
|
|
|
|
where, args := buildUserListWhere(req, time.Now().Unix())
|
|
var total int64
|
|
if err := conn.Table(comm.TableUser).Where(where, args...).Count(&total).Error; err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
users := make([]*pb.DBUser, 0, req.PageSize)
|
|
if total > 0 {
|
|
if err := conn.Table(comm.TableUser).
|
|
Where(where, args...).
|
|
Order("createtime DESC").
|
|
Limit(req.PageSize).Offset(req.Page * req.PageSize).
|
|
Find(&users).Error; err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
}
|
|
for _, u := range users {
|
|
u.Password = "" // 列表同样不下发密码
|
|
}
|
|
writeOK(c, gin.H{"users": users, "total": total, "page": req.Page, "page_size": req.PageSize})
|
|
}
|
|
|
|
// updateUser 修改终端用户的可运营参数。
|
|
//
|
|
// ⚠️ 只放给超管/管理员。代理(Agent)不在此列——代理要发资源必须走
|
|
// api_giftuserresource,那条路会从代理自己的余额里扣;开放本接口等于让代理凭空造资源。
|
|
//
|
|
// ⚠️ 只改白名单里的几个字段,用 Updates(map) 局部更新而不是整行 Save:
|
|
// 整行写回会把并发中的登录时间、积分消耗等一起覆盖成后台读到的旧值。
|
|
func (this *serverComp) updateUser(c *gin.Context) {
|
|
if idt := currentIdentity(c); idt != pb.Identity_Admin && idt != pb.Identity_Manager {
|
|
writeErr(c, pb.ErrorCode_InsufficientPermissions, "仅超管/管理员可修改用户参数(代理请用「赠送资源点」)")
|
|
return
|
|
}
|
|
appId := parseAppId(c.GetHeader("X-App-Id"))
|
|
if !this.requireAppScope(c, appId) {
|
|
return
|
|
}
|
|
conn, err := this.module.registry.getServiceDB(appId)
|
|
if err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
// 指针字段:区分「没传这一项」与「传了 0」。用值类型会让「把积分清零」变成不可表达的操作。
|
|
var req struct {
|
|
Uid string `json:"uid"`
|
|
Name *string `json:"name"`
|
|
State *int32 `json:"state"`
|
|
Viplv *int32 `json:"viplv"`
|
|
Vipexptime *int64 `json:"vipexptime"`
|
|
Aichatintegral *int64 `json:"aichatintegral"`
|
|
Tradeintegral *int64 `json:"tradeintegral"`
|
|
Meetintegral *int64 `json:"meetintegral"`
|
|
}
|
|
_ = c.ShouldBindJSON(&req)
|
|
uid := strings.TrimSpace(req.Uid)
|
|
if uid == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "uid 必填")
|
|
return
|
|
}
|
|
old := &pb.DBUser{}
|
|
if err := conn.FindOne(comm.TableUser, old, "uid=?", uid); err != nil {
|
|
if errors.Is(err, mysql.ErrNoDocuments) {
|
|
writeErr(c, pb.ErrorCode_UserSessionNobeing, "未找到该用户")
|
|
return
|
|
}
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
|
|
updates := map[string]interface{}{}
|
|
if req.Name != nil {
|
|
updates["name"] = strings.TrimSpace(*req.Name)
|
|
}
|
|
if req.State != nil {
|
|
switch *req.State {
|
|
case int32(pb.UserState_Normal), int32(pb.UserState_Disabled), int32(pb.UserState_Cancelled):
|
|
updates["State"] = *req.State
|
|
default:
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "状态仅支持 0(正常) / 1(禁用) / 2(已注销)")
|
|
return
|
|
}
|
|
}
|
|
if req.Viplv != nil {
|
|
if *req.Viplv < 0 {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "VIP 等级不能为负")
|
|
return
|
|
}
|
|
updates["viplv"] = *req.Viplv
|
|
}
|
|
if req.Vipexptime != nil {
|
|
if *req.Vipexptime < 0 {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "VIP 到期时间不能为负")
|
|
return
|
|
}
|
|
updates["vipexptime"] = *req.Vipexptime
|
|
}
|
|
// 三种积分是「当前余额」,直接设成绝对值。总额(total*)不动——那是累计发放量,
|
|
// 属于流水统计口径,被后台改一次就再也对不上账了。
|
|
for k, v := range map[string]*int64{
|
|
"aichatintegral": req.Aichatintegral,
|
|
"tradeintegral": req.Tradeintegral,
|
|
"meetintegral": req.Meetintegral,
|
|
} {
|
|
if v == nil {
|
|
continue
|
|
}
|
|
if *v < 0 {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "积分余额不能为负")
|
|
return
|
|
}
|
|
updates[k] = *v
|
|
}
|
|
if len(updates) == 0 {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "没有要修改的字段")
|
|
return
|
|
}
|
|
if err := conn.Table(comm.TableUser).Where("uid=?", uid).Updates(updates).Error; err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
writeOK(c, gin.H{"uid": uid, "updated": len(updates)})
|
|
}
|
|
|