923 lines
30 KiB
Go
923 lines
30 KiB
Go
package controllers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"server/models"
|
||
"server/pkg/jwtutil"
|
||
|
||
"github.com/beego/beego/v2/client/orm"
|
||
"github.com/beego/beego/v2/core/logs"
|
||
beego "github.com/beego/beego/v2/server/web"
|
||
)
|
||
|
||
type passwordPayload struct {
|
||
Platform string `json:"platform"`
|
||
URL string `json:"url"`
|
||
Accounts []models.PasswordAccountItem `json:"accounts"`
|
||
Remark string `json:"remark"`
|
||
}
|
||
|
||
// PasswordStoreItemDTO 统一返回给前端的结构体,包含解析后的账号列表
|
||
type PasswordStoreItemDTO struct {
|
||
ID uint64 `json:"id"`
|
||
Tid int `json:"tid,omitempty"`
|
||
Platform string `json:"platform"`
|
||
URL string `json:"url"`
|
||
Accounts []models.PasswordAccountItem `json:"accounts"`
|
||
Remark string `json:"remark"`
|
||
UserID uint64 `json:"user_id"`
|
||
CreateTime time.Time `json:"create_time"`
|
||
UpdateTime *time.Time `json:"update_time"`
|
||
}
|
||
|
||
func accountsToJSON(list []models.PasswordAccountItem) string {
|
||
cleaned := make([]models.PasswordAccountItem, 0, len(list))
|
||
for _, item := range list {
|
||
u := strings.TrimSpace(item.Username)
|
||
p := strings.TrimSpace(item.Password)
|
||
reg := strings.TrimSpace(item.RegistrationInfo)
|
||
rem := strings.TrimSpace(item.Remark)
|
||
if u == "" && p == "" && reg == "" && rem == "" {
|
||
continue
|
||
}
|
||
cleaned = append(cleaned, models.PasswordAccountItem{
|
||
Username: u,
|
||
Password: p,
|
||
RegistrationInfo: reg,
|
||
Remark: rem,
|
||
})
|
||
}
|
||
b, err := json.Marshal(cleaned)
|
||
if err != nil {
|
||
return "[]"
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
func accountsFromJSON(raw string) []models.PasswordAccountItem {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return []models.PasswordAccountItem{}
|
||
}
|
||
var list []models.PasswordAccountItem
|
||
if err := json.Unmarshal([]byte(raw), &list); err == nil && list != nil {
|
||
return list
|
||
}
|
||
return []models.PasswordAccountItem{}
|
||
}
|
||
|
||
func passwordClaims(c *beego.Controller, kind string) (*jwtutil.Claims, error) {
|
||
p := strings.SplitN(c.Ctx.Request.Header.Get("Authorization"), " ", 2)
|
||
if len(p) != 2 || p[0] != "Bearer" {
|
||
return nil, orm.ErrNoRows
|
||
}
|
||
cl, e := jwtutil.ParseToken(p[1])
|
||
if e != nil || cl.UserType != kind || cl.UserID <= 0 {
|
||
return nil, orm.ErrNoRows
|
||
}
|
||
return cl, nil
|
||
}
|
||
|
||
func parsePassword(c *beego.Controller) (passwordPayload, error) {
|
||
var p passwordPayload
|
||
b, e := io.ReadAll(c.Ctx.Request.Body)
|
||
if e == nil {
|
||
e = json.Unmarshal(b, &p)
|
||
}
|
||
p.Platform = strings.TrimSpace(p.Platform)
|
||
p.URL = strings.TrimSpace(p.URL)
|
||
p.Remark = strings.TrimSpace(p.Remark)
|
||
return p, e
|
||
}
|
||
|
||
func passwordReply(c *beego.Controller, status int, msg string, data interface{}) {
|
||
c.Ctx.Output.SetStatus(status)
|
||
c.Data["json"] = map[string]interface{}{"code": status, "msg": msg, "data": data}
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
func validPassword(p passwordPayload) bool {
|
||
return p.Platform != ""
|
||
}
|
||
|
||
// sameAccount 判定两个账号是否重复:
|
||
// 登录账号都非空时按账号比较(忽略大小写);账号为空时按密码比较。
|
||
func sameAccount(a, b models.PasswordAccountItem) bool {
|
||
u1 := strings.TrimSpace(a.Username)
|
||
u2 := strings.TrimSpace(b.Username)
|
||
if u1 != "" && u2 != "" {
|
||
return strings.EqualFold(u1, u2)
|
||
}
|
||
if u1 != u2 {
|
||
return false
|
||
}
|
||
return strings.TrimSpace(a.Password) == strings.TrimSpace(b.Password)
|
||
}
|
||
|
||
// validateAccountsUnique 校验同一平台下的登录账号不重复
|
||
func validateAccountsUnique(list []models.PasswordAccountItem) error {
|
||
seen := map[string]bool{}
|
||
for _, item := range list {
|
||
u := strings.TrimSpace(item.Username)
|
||
if u == "" {
|
||
continue
|
||
}
|
||
key := strings.ToLower(u)
|
||
if seen[key] {
|
||
return fmt.Errorf("同一平台下登录账号「%s」重复,请合并后再保存", u)
|
||
}
|
||
seen[key] = true
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// dedupeAccounts 去掉同组内的重复账号(保留首次出现的一条),返回去重后的列表与去重数量
|
||
func dedupeAccounts(list []models.PasswordAccountItem) ([]models.PasswordAccountItem, int) {
|
||
out := make([]models.PasswordAccountItem, 0, len(list))
|
||
removed := 0
|
||
for _, item := range list {
|
||
dup := false
|
||
for _, keep := range out {
|
||
if sameAccount(keep, item) {
|
||
dup = true
|
||
break
|
||
}
|
||
}
|
||
if dup {
|
||
removed++
|
||
continue
|
||
}
|
||
out = append(out, item)
|
||
}
|
||
return out, removed
|
||
}
|
||
|
||
// findSamePlatformRecord 查询同用户(同租户)下是否已存在「平台名称 + 网址」相同的记录
|
||
func findSamePlatformRecord(qs orm.QuerySeter, excludeID uint64, platform, url string) (uint64, error) {
|
||
q := qs.Filter("is_deleted", 0).Filter("platform", platform).Filter("url", url)
|
||
if excludeID > 0 {
|
||
q = q.Exclude("id", excludeID)
|
||
}
|
||
// 用 Values 只取 ID,避免不同模型的 One() 类型差异
|
||
var rows []orm.Params
|
||
if _, err := q.OrderBy("id").Limit(1).Values(&rows, "id"); err != nil {
|
||
return 0, err
|
||
}
|
||
if len(rows) == 0 {
|
||
return 0, orm.ErrNoRows
|
||
}
|
||
id, _ := strconv.ParseUint(fmt.Sprintf("%v", rows[0]["Id"]), 10, 64)
|
||
if id == 0 {
|
||
id, _ = strconv.ParseUint(fmt.Sprintf("%v", rows[0]["id"]), 10, 64)
|
||
}
|
||
if id == 0 {
|
||
return 0, orm.ErrNoRows
|
||
}
|
||
return id, nil
|
||
}
|
||
|
||
// passwordPageParams 解析分页参数:page 默认 1,pageSize 默认 20、上限 200
|
||
func passwordPageParams(c *beego.Controller) (page, pageSize int) {
|
||
page, _ = c.GetInt("page", 1)
|
||
pageSize, _ = c.GetInt("pageSize", 20)
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 200 {
|
||
pageSize = 20
|
||
}
|
||
return page, pageSize
|
||
}
|
||
|
||
// passwordPageReply 分页列表统一响应(list + total + 当前分页信息)
|
||
func passwordPageReply(c *beego.Controller, msg string, list interface{}, total int64, page, pageSize int) {
|
||
passwordReply(c, 200, msg, map[string]interface{}{
|
||
"list": list,
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": pageSize,
|
||
})
|
||
}
|
||
|
||
// ==================== 平台端 Password Store ====================
|
||
|
||
type PlatformPasswordStoreController struct{ beego.Controller }
|
||
|
||
func (c *PlatformPasswordStoreController) List() {
|
||
cl, e := passwordClaims(&c.Controller, "platform")
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
qs := models.Orm.QueryTable(new(models.PlatformPasswordStore)).Filter("is_deleted", 0).Filter("user_id", cl.UserID)
|
||
k := strings.TrimSpace(c.GetString("keyword"))
|
||
if k != "" {
|
||
cond := orm.NewCondition().
|
||
Or("platform__icontains", k).
|
||
Or("url__icontains", k).
|
||
Or("accounts__icontains", k).
|
||
Or("remark__icontains", k)
|
||
qs = qs.SetCond(orm.NewCondition().And("is_deleted", 0).And("user_id", cl.UserID).AndCond(cond))
|
||
}
|
||
// 分页:默认每页 20 条,单页最多 200 条
|
||
page, pageSize := passwordPageParams(&c.Controller)
|
||
total, _ := qs.Count()
|
||
var list []models.PlatformPasswordStore
|
||
_, e = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&list)
|
||
if e != nil && e != orm.ErrNoRows {
|
||
logs.Error("[passwordStore] platform list failed: %v", e)
|
||
passwordReply(&c.Controller, 500, "查询失败: "+e.Error(), nil)
|
||
return
|
||
}
|
||
res := make([]PasswordStoreItemDTO, 0, len(list))
|
||
for _, row := range list {
|
||
res = append(res, PasswordStoreItemDTO{
|
||
ID: row.ID,
|
||
Platform: row.Platform,
|
||
URL: row.URL,
|
||
Accounts: accountsFromJSON(row.Accounts),
|
||
Remark: row.Remark,
|
||
UserID: row.UserID,
|
||
CreateTime: row.CreateTime,
|
||
UpdateTime: row.UpdateTime,
|
||
})
|
||
}
|
||
passwordPageReply(&c.Controller, "success", res, total, page, pageSize)
|
||
}
|
||
|
||
func (c *PlatformPasswordStoreController) Detail() {
|
||
cl, e := passwordClaims(&c.Controller, "platform")
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||
var v models.PlatformPasswordStore
|
||
e = models.Orm.QueryTable(new(models.PlatformPasswordStore)).Filter("id", id).Filter("is_deleted", 0).Filter("user_id", cl.UserID).One(&v)
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||
return
|
||
}
|
||
dto := PasswordStoreItemDTO{
|
||
ID: v.ID,
|
||
Platform: v.Platform,
|
||
URL: v.URL,
|
||
Accounts: accountsFromJSON(v.Accounts),
|
||
Remark: v.Remark,
|
||
UserID: v.UserID,
|
||
CreateTime: v.CreateTime,
|
||
UpdateTime: v.UpdateTime,
|
||
}
|
||
passwordReply(&c.Controller, 200, "success", dto)
|
||
}
|
||
|
||
func (c *PlatformPasswordStoreController) Create() {
|
||
cl, e := passwordClaims(&c.Controller, "platform")
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
p, e := parsePassword(&c.Controller)
|
||
if e != nil || !validPassword(p) {
|
||
passwordReply(&c.Controller, 400, "平台名称不能为空", nil)
|
||
return
|
||
}
|
||
// 重复检测:平台名称 + 网址 已存在则拒绝新增
|
||
if existID, ferr := findSamePlatformRecord(
|
||
models.Orm.QueryTable(new(models.PlatformPasswordStore)).Filter("user_id", cl.UserID),
|
||
0, p.Platform, p.URL); ferr == nil && existID > 0 {
|
||
passwordReply(&c.Controller, 400, fmt.Sprintf("已存在同名同网址的平台「%s」(ID %d),请直接编辑该记录", p.Platform, existID), nil)
|
||
return
|
||
}
|
||
// 重复检测:同一平台下的登录账号不能重复
|
||
if verr := validateAccountsUnique(p.Accounts); verr != nil {
|
||
passwordReply(&c.Controller, 400, verr.Error(), nil)
|
||
return
|
||
}
|
||
accJSON := accountsToJSON(p.Accounts)
|
||
v := &models.PlatformPasswordStore{
|
||
Platform: p.Platform,
|
||
URL: p.URL,
|
||
Accounts: accJSON,
|
||
Remark: p.Remark,
|
||
UserID: uint64(cl.UserID),
|
||
}
|
||
id, e := models.Orm.Insert(v)
|
||
if e != nil {
|
||
logs.Error("[passwordStore] platform create failed: %v", e)
|
||
passwordReply(&c.Controller, 500, "创建失败: "+e.Error(), nil)
|
||
return
|
||
}
|
||
v.ID = uint64(id)
|
||
dto := PasswordStoreItemDTO{
|
||
ID: v.ID,
|
||
Platform: v.Platform,
|
||
URL: v.URL,
|
||
Accounts: accountsFromJSON(v.Accounts),
|
||
Remark: v.Remark,
|
||
UserID: v.UserID,
|
||
CreateTime: v.CreateTime,
|
||
UpdateTime: v.UpdateTime,
|
||
}
|
||
passwordReply(&c.Controller, 200, "创建成功", dto)
|
||
}
|
||
|
||
func (c *PlatformPasswordStoreController) Update() {
|
||
cl, e := passwordClaims(&c.Controller, "platform")
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||
p, e := parsePassword(&c.Controller)
|
||
if e != nil || !validPassword(p) {
|
||
passwordReply(&c.Controller, 400, "平台名称不能为空", nil)
|
||
return
|
||
}
|
||
var v models.PlatformPasswordStore
|
||
e = models.Orm.QueryTable(new(models.PlatformPasswordStore)).Filter("id", id).Filter("is_deleted", 0).Filter("user_id", cl.UserID).One(&v)
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||
return
|
||
}
|
||
// 重复检测:改成的平台名称 + 网址 不能与自己的其它记录冲突
|
||
if existID, ferr := findSamePlatformRecord(
|
||
models.Orm.QueryTable(new(models.PlatformPasswordStore)).Filter("user_id", cl.UserID),
|
||
id, p.Platform, p.URL); ferr == nil && existID > 0 {
|
||
passwordReply(&c.Controller, 400, fmt.Sprintf("已存在同名同网址的平台「%s」(ID %d),请先合并或改名", p.Platform, existID), nil)
|
||
return
|
||
}
|
||
// 重复检测:同一平台下的登录账号不能重复
|
||
if verr := validateAccountsUnique(p.Accounts); verr != nil {
|
||
passwordReply(&c.Controller, 400, verr.Error(), nil)
|
||
return
|
||
}
|
||
now := time.Now()
|
||
accJSON := accountsToJSON(p.Accounts)
|
||
_, e = models.Orm.QueryTable(new(models.PlatformPasswordStore)).Filter("id", id).Update(map[string]interface{}{
|
||
"platform": p.Platform,
|
||
"url": p.URL,
|
||
"accounts": accJSON,
|
||
"remark": p.Remark,
|
||
"update_time": now,
|
||
})
|
||
if e != nil {
|
||
logs.Error("[passwordStore] platform update failed: %v", e)
|
||
passwordReply(&c.Controller, 500, "更新失败: "+e.Error(), nil)
|
||
return
|
||
}
|
||
v.Platform = p.Platform
|
||
v.URL = p.URL
|
||
v.Accounts = accJSON
|
||
v.Remark = p.Remark
|
||
v.UpdateTime = &now
|
||
dto := PasswordStoreItemDTO{
|
||
ID: v.ID,
|
||
Platform: v.Platform,
|
||
URL: v.URL,
|
||
Accounts: accountsFromJSON(v.Accounts),
|
||
Remark: v.Remark,
|
||
UserID: v.UserID,
|
||
CreateTime: v.CreateTime,
|
||
UpdateTime: v.UpdateTime,
|
||
}
|
||
passwordReply(&c.Controller, 200, "更新成功", dto)
|
||
}
|
||
|
||
func (c *PlatformPasswordStoreController) Delete() {
|
||
cl, e := passwordClaims(&c.Controller, "platform")
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||
n, e := models.Orm.QueryTable(new(models.PlatformPasswordStore)).Filter("id", id).Filter("user_id", cl.UserID).Update(map[string]interface{}{"is_deleted": 1, "delete_time": time.Now()})
|
||
if e != nil || n == 0 {
|
||
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||
return
|
||
}
|
||
passwordReply(&c.Controller, 200, "删除成功", nil)
|
||
}
|
||
|
||
// ==================== 租户端 Password Store ====================
|
||
|
||
type BackendPasswordStoreController struct{ beego.Controller }
|
||
|
||
func (c *BackendPasswordStoreController) List() {
|
||
cl, e := passwordClaims(&c.Controller, "backend")
|
||
if e != nil || cl.TenantId <= 0 {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
qs := models.Orm.QueryTable(new(models.BackendPasswordStore)).Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID)
|
||
k := strings.TrimSpace(c.GetString("keyword"))
|
||
if k != "" {
|
||
cond := orm.NewCondition().
|
||
Or("platform__icontains", k).
|
||
Or("url__icontains", k).
|
||
Or("accounts__icontains", k).
|
||
Or("remark__icontains", k)
|
||
qs = qs.SetCond(orm.NewCondition().And("is_deleted", 0).And("tid", cl.TenantId).And("user_id", cl.UserID).AndCond(cond))
|
||
}
|
||
// 分页:默认每页 20 条,单页最多 200 条
|
||
page, pageSize := passwordPageParams(&c.Controller)
|
||
total, _ := qs.Count()
|
||
var list []models.BackendPasswordStore
|
||
_, e = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&list)
|
||
if e != nil && e != orm.ErrNoRows {
|
||
logs.Error("[passwordStore] backend list failed: %v", e)
|
||
passwordReply(&c.Controller, 500, "查询失败: "+e.Error(), nil)
|
||
return
|
||
}
|
||
res := make([]PasswordStoreItemDTO, 0, len(list))
|
||
for _, row := range list {
|
||
res = append(res, PasswordStoreItemDTO{
|
||
ID: row.ID,
|
||
Tid: row.Tid,
|
||
Platform: row.Platform,
|
||
URL: row.URL,
|
||
Accounts: accountsFromJSON(row.Accounts),
|
||
Remark: row.Remark,
|
||
UserID: row.UserID,
|
||
CreateTime: row.CreateTime,
|
||
UpdateTime: row.UpdateTime,
|
||
})
|
||
}
|
||
passwordPageReply(&c.Controller, "success", res, total, page, pageSize)
|
||
}
|
||
|
||
func (c *BackendPasswordStoreController) Detail() {
|
||
cl, e := passwordClaims(&c.Controller, "backend")
|
||
if e != nil || cl.TenantId <= 0 {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||
var v models.BackendPasswordStore
|
||
e = models.Orm.QueryTable(new(models.BackendPasswordStore)).Filter("id", id).Filter("tid", cl.TenantId).Filter("is_deleted", 0).Filter("user_id", cl.UserID).One(&v)
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||
return
|
||
}
|
||
dto := PasswordStoreItemDTO{
|
||
ID: v.ID,
|
||
Tid: v.Tid,
|
||
Platform: v.Platform,
|
||
URL: v.URL,
|
||
Accounts: accountsFromJSON(v.Accounts),
|
||
Remark: v.Remark,
|
||
UserID: v.UserID,
|
||
CreateTime: v.CreateTime,
|
||
UpdateTime: v.UpdateTime,
|
||
}
|
||
passwordReply(&c.Controller, 200, "success", dto)
|
||
}
|
||
|
||
func (c *BackendPasswordStoreController) Create() {
|
||
cl, e := passwordClaims(&c.Controller, "backend")
|
||
if e != nil || cl.TenantId <= 0 {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
p, e := parsePassword(&c.Controller)
|
||
if e != nil || !validPassword(p) {
|
||
passwordReply(&c.Controller, 400, "平台名称不能为空", nil)
|
||
return
|
||
}
|
||
// 重复检测:同租户下平台名称 + 网址 已存在则拒绝新增
|
||
if existID, ferr := findSamePlatformRecord(
|
||
models.Orm.QueryTable(new(models.BackendPasswordStore)).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID),
|
||
0, p.Platform, p.URL); ferr == nil && existID > 0 {
|
||
passwordReply(&c.Controller, 400, fmt.Sprintf("已存在同名同网址的平台「%s」(ID %d),请直接编辑该记录", p.Platform, existID), nil)
|
||
return
|
||
}
|
||
// 重复检测:同一平台下的登录账号不能重复
|
||
if verr := validateAccountsUnique(p.Accounts); verr != nil {
|
||
passwordReply(&c.Controller, 400, verr.Error(), nil)
|
||
return
|
||
}
|
||
accJSON := accountsToJSON(p.Accounts)
|
||
v := &models.BackendPasswordStore{
|
||
Tid: cl.TenantId,
|
||
Platform: p.Platform,
|
||
URL: p.URL,
|
||
Accounts: accJSON,
|
||
Remark: p.Remark,
|
||
UserID: uint64(cl.UserID),
|
||
}
|
||
id, e := models.Orm.Insert(v)
|
||
if e != nil {
|
||
logs.Error("[passwordStore] backend create failed: %v", e)
|
||
passwordReply(&c.Controller, 500, "创建失败: "+e.Error(), nil)
|
||
return
|
||
}
|
||
v.ID = uint64(id)
|
||
dto := PasswordStoreItemDTO{
|
||
ID: v.ID,
|
||
Tid: v.Tid,
|
||
Platform: v.Platform,
|
||
URL: v.URL,
|
||
Accounts: accountsFromJSON(v.Accounts),
|
||
Remark: v.Remark,
|
||
UserID: v.UserID,
|
||
CreateTime: v.CreateTime,
|
||
UpdateTime: v.UpdateTime,
|
||
}
|
||
passwordReply(&c.Controller, 200, "创建成功", dto)
|
||
}
|
||
|
||
func (c *BackendPasswordStoreController) Update() {
|
||
cl, e := passwordClaims(&c.Controller, "backend")
|
||
if e != nil || cl.TenantId <= 0 {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||
p, e := parsePassword(&c.Controller)
|
||
if e != nil || !validPassword(p) {
|
||
passwordReply(&c.Controller, 400, "平台名称不能为空", nil)
|
||
return
|
||
}
|
||
var v models.BackendPasswordStore
|
||
e = models.Orm.QueryTable(new(models.BackendPasswordStore)).Filter("id", id).Filter("tid", cl.TenantId).Filter("is_deleted", 0).Filter("user_id", cl.UserID).One(&v)
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||
return
|
||
}
|
||
// 重复检测:改成的平台名称 + 网址 不能与自己的其它记录冲突
|
||
if existID, ferr := findSamePlatformRecord(
|
||
models.Orm.QueryTable(new(models.BackendPasswordStore)).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID),
|
||
id, p.Platform, p.URL); ferr == nil && existID > 0 {
|
||
passwordReply(&c.Controller, 400, fmt.Sprintf("已存在同名同网址的平台「%s」(ID %d),请先合并或改名", p.Platform, existID), nil)
|
||
return
|
||
}
|
||
// 重复检测:同一平台下的登录账号不能重复
|
||
if verr := validateAccountsUnique(p.Accounts); verr != nil {
|
||
passwordReply(&c.Controller, 400, verr.Error(), nil)
|
||
return
|
||
}
|
||
now := time.Now()
|
||
accJSON := accountsToJSON(p.Accounts)
|
||
_, e = models.Orm.QueryTable(new(models.BackendPasswordStore)).Filter("id", id).Update(map[string]interface{}{
|
||
"platform": p.Platform,
|
||
"url": p.URL,
|
||
"accounts": accJSON,
|
||
"remark": p.Remark,
|
||
"update_time": now,
|
||
})
|
||
if e != nil {
|
||
logs.Error("[passwordStore] backend update failed: %v", e)
|
||
passwordReply(&c.Controller, 500, "更新失败: "+e.Error(), nil)
|
||
return
|
||
}
|
||
v.Platform = p.Platform
|
||
v.URL = p.URL
|
||
v.Accounts = accJSON
|
||
v.Remark = p.Remark
|
||
v.UpdateTime = &now
|
||
dto := PasswordStoreItemDTO{
|
||
ID: v.ID,
|
||
Tid: v.Tid,
|
||
Platform: v.Platform,
|
||
URL: v.URL,
|
||
Accounts: accountsFromJSON(v.Accounts),
|
||
Remark: v.Remark,
|
||
UserID: v.UserID,
|
||
CreateTime: v.CreateTime,
|
||
UpdateTime: v.UpdateTime,
|
||
}
|
||
passwordReply(&c.Controller, 200, "更新成功", dto)
|
||
}
|
||
|
||
func (c *BackendPasswordStoreController) Delete() {
|
||
cl, e := passwordClaims(&c.Controller, "backend")
|
||
if e != nil || cl.TenantId <= 0 {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||
n, e := models.Orm.QueryTable(new(models.BackendPasswordStore)).Filter("id", id).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID).Update(map[string]interface{}{"is_deleted": 1, "delete_time": time.Now()})
|
||
if e != nil || n == 0 {
|
||
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||
return
|
||
}
|
||
passwordReply(&c.Controller, 200, "删除成功", nil)
|
||
}
|
||
|
||
// ==================== 批量导入 / 导出 ====================
|
||
//
|
||
// Excel 列顺序(与数据库字段一一对应):
|
||
// 平台名称 platform | 网址 url | 账号 username | 密码 password |
|
||
// 注册信息 registration_info | 账号备注 account_remark | 备注 remark
|
||
// 一个平台可挂多个账号:多行「平台名称 + 网址」相同即合并为一条记录的多个账号;
|
||
// 已存在的同平台同网址记录按「追加账号(跳过完全重复项)」处理。
|
||
|
||
type passwordImportRow struct {
|
||
Platform string `json:"platform"`
|
||
URL string `json:"url"`
|
||
Username string `json:"username"`
|
||
Password string `json:"password"`
|
||
RegistrationInfo string `json:"registration_info"`
|
||
AccountRemark string `json:"account_remark"`
|
||
Remark string `json:"remark"`
|
||
}
|
||
|
||
type passwordImportResult struct {
|
||
Created int `json:"created"`
|
||
Updated int `json:"updated"`
|
||
Skipped int `json:"skipped"`
|
||
Failed int `json:"failed"`
|
||
Failures []string `json:"failures"`
|
||
}
|
||
|
||
type passwordImportGroup struct {
|
||
Platform string
|
||
URL string
|
||
Remark string
|
||
Accounts []models.PasswordAccountItem
|
||
}
|
||
|
||
func parsePasswordImportRows(c *beego.Controller) ([]passwordImportRow, error) {
|
||
b, e := io.ReadAll(c.Ctx.Request.Body)
|
||
var p struct {
|
||
Rows []passwordImportRow `json:"rows"`
|
||
}
|
||
if e == nil {
|
||
e = json.Unmarshal(b, &p)
|
||
}
|
||
return p.Rows, e
|
||
}
|
||
|
||
// groupPasswordImportRows 按「平台名称 + 网址」聚合成记录,返回有序分组与行级错误
|
||
func groupPasswordImportRows(rows []passwordImportRow) ([]*passwordImportGroup, []string) {
|
||
order := make([]string, 0, len(rows))
|
||
index := map[string]*passwordImportGroup{}
|
||
failures := make([]string, 0)
|
||
for i, r := range rows {
|
||
platform := strings.TrimSpace(r.Platform)
|
||
if platform == "" {
|
||
failures = append(failures, fmt.Sprintf("第%d行:平台名称不能为空", i+2))
|
||
continue
|
||
}
|
||
url := strings.TrimSpace(r.URL)
|
||
username := strings.TrimSpace(r.Username)
|
||
pwd := strings.TrimSpace(r.Password)
|
||
reg := strings.TrimSpace(r.RegistrationInfo)
|
||
accRemark := strings.TrimSpace(r.AccountRemark)
|
||
remark := strings.TrimSpace(r.Remark)
|
||
|
||
key := platform + "\n" + url
|
||
g, ok := index[key]
|
||
if !ok {
|
||
g = &passwordImportGroup{Platform: platform, URL: url}
|
||
index[key] = g
|
||
order = append(order, key)
|
||
}
|
||
if username != "" || pwd != "" || reg != "" || accRemark != "" {
|
||
item := models.PasswordAccountItem{
|
||
Username: username,
|
||
Password: pwd,
|
||
RegistrationInfo: reg,
|
||
Remark: accRemark,
|
||
}
|
||
// 文件内重复检测:同一「平台名称 + 网址」下登录账号重复则跳过
|
||
dup := false
|
||
for _, keep := range g.Accounts {
|
||
if sameAccount(keep, item) {
|
||
dup = true
|
||
break
|
||
}
|
||
}
|
||
if dup {
|
||
label := username
|
||
if label == "" {
|
||
label = "(空账号)"
|
||
}
|
||
failures = append(failures, fmt.Sprintf("第%d行:平台「%s」下账号「%s」与前面某行重复,已跳过", i+2, platform, label))
|
||
continue
|
||
}
|
||
g.Accounts = append(g.Accounts, item)
|
||
}
|
||
if remark != "" {
|
||
g.Remark = remark
|
||
}
|
||
}
|
||
out := make([]*passwordImportGroup, 0, len(order))
|
||
for _, k := range order {
|
||
out = append(out, index[k])
|
||
}
|
||
return out, failures
|
||
}
|
||
|
||
// mergeAccounts 追加导入账号到已有账号之后,返回合并结果与「实际新增条数」。
|
||
// 重复判定:登录账号相同(忽略大小写)即视为重复,账号为空时按密码比较。
|
||
func mergeAccounts(exist, incoming []models.PasswordAccountItem) ([]models.PasswordAccountItem, int) {
|
||
out := make([]models.PasswordAccountItem, 0, len(exist)+len(incoming))
|
||
out = append(out, exist...)
|
||
added := 0
|
||
for _, in := range incoming {
|
||
dup := false
|
||
for _, e := range exist {
|
||
if sameAccount(e, in) {
|
||
dup = true
|
||
break
|
||
}
|
||
}
|
||
if dup {
|
||
continue
|
||
}
|
||
out = append(out, in)
|
||
added++
|
||
}
|
||
return out, added
|
||
}
|
||
|
||
// Import POST /platform/passwordStore/import { rows: [...] }
|
||
func (c *PlatformPasswordStoreController) Import() {
|
||
cl, e := passwordClaims(&c.Controller, "platform")
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
rows, e := parsePasswordImportRows(&c.Controller)
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 400, "参数错误", nil)
|
||
return
|
||
}
|
||
uid := uint64(cl.UserID)
|
||
groups, failures := groupPasswordImportRows(rows)
|
||
res := passwordImportResult{Failed: len(failures), Failures: failures}
|
||
now := time.Now()
|
||
|
||
for _, g := range groups {
|
||
var exist models.PlatformPasswordStore
|
||
err := models.Orm.QueryTable(new(models.PlatformPasswordStore)).
|
||
Filter("is_deleted", 0).Filter("user_id", uid).
|
||
Filter("platform", g.Platform).Filter("url", g.URL).One(&exist)
|
||
if err == nil {
|
||
merged, added := mergeAccounts(accountsFromJSON(exist.Accounts), g.Accounts)
|
||
if added == 0 {
|
||
res.Skipped++
|
||
res.Failures = append(res.Failures, fmt.Sprintf("平台「%s」的登录账号已全部存在,已跳过", g.Platform))
|
||
continue
|
||
}
|
||
accounts := accountsToJSON(merged)
|
||
remark := exist.Remark
|
||
if g.Remark != "" {
|
||
remark = g.Remark
|
||
}
|
||
if _, uerr := models.Orm.QueryTable(new(models.PlatformPasswordStore)).Filter("id", exist.ID).
|
||
Update(map[string]interface{}{"accounts": accounts, "remark": remark, "update_time": now}); uerr != nil {
|
||
res.Failed++
|
||
res.Failures = append(res.Failures, fmt.Sprintf("平台「%s」更新失败:%s", g.Platform, uerr.Error()))
|
||
continue
|
||
}
|
||
res.Updated++
|
||
continue
|
||
}
|
||
v := &models.PlatformPasswordStore{
|
||
Platform: g.Platform,
|
||
URL: g.URL,
|
||
Accounts: accountsToJSON(g.Accounts),
|
||
Remark: g.Remark,
|
||
UserID: uid,
|
||
}
|
||
if _, ierr := models.Orm.Insert(v); ierr != nil {
|
||
res.Failed++
|
||
res.Failures = append(res.Failures, fmt.Sprintf("平台「%s」新增失败:%s", g.Platform, ierr.Error()))
|
||
continue
|
||
}
|
||
res.Created++
|
||
}
|
||
passwordReply(&c.Controller, 200, "导入完成", res)
|
||
}
|
||
|
||
// Export GET /platform/passwordStore/export 导出全部(不受列表 200 条限制)
|
||
func (c *PlatformPasswordStoreController) Export() {
|
||
cl, e := passwordClaims(&c.Controller, "platform")
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
var list []models.PlatformPasswordStore
|
||
if _, e = models.Orm.QueryTable(new(models.PlatformPasswordStore)).
|
||
Filter("is_deleted", 0).Filter("user_id", cl.UserID).
|
||
OrderBy("-id").All(&list); e != nil && e != orm.ErrNoRows {
|
||
passwordReply(&c.Controller, 500, "查询失败: "+e.Error(), nil)
|
||
return
|
||
}
|
||
res := make([]PasswordStoreItemDTO, 0, len(list))
|
||
for _, row := range list {
|
||
res = append(res, PasswordStoreItemDTO{
|
||
ID: row.ID,
|
||
Platform: row.Platform,
|
||
URL: row.URL,
|
||
Accounts: accountsFromJSON(row.Accounts),
|
||
Remark: row.Remark,
|
||
UserID: row.UserID,
|
||
CreateTime: row.CreateTime,
|
||
UpdateTime: row.UpdateTime,
|
||
})
|
||
}
|
||
passwordReply(&c.Controller, 200, "success", map[string]interface{}{"list": res, "total": len(res)})
|
||
}
|
||
|
||
// Import POST /backend/passwordStore/import { rows: [...] }
|
||
func (c *BackendPasswordStoreController) Import() {
|
||
cl, e := passwordClaims(&c.Controller, "backend")
|
||
if e != nil || cl.TenantId <= 0 {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
rows, e := parsePasswordImportRows(&c.Controller)
|
||
if e != nil {
|
||
passwordReply(&c.Controller, 400, "参数错误", nil)
|
||
return
|
||
}
|
||
uid := uint64(cl.UserID)
|
||
tid := cl.TenantId
|
||
groups, failures := groupPasswordImportRows(rows)
|
||
res := passwordImportResult{Failed: len(failures), Failures: failures}
|
||
now := time.Now()
|
||
|
||
for _, g := range groups {
|
||
var exist models.BackendPasswordStore
|
||
err := models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
||
Filter("is_deleted", 0).Filter("tid", tid).Filter("user_id", uid).
|
||
Filter("platform", g.Platform).Filter("url", g.URL).One(&exist)
|
||
if err == nil {
|
||
merged, added := mergeAccounts(accountsFromJSON(exist.Accounts), g.Accounts)
|
||
if added == 0 {
|
||
res.Skipped++
|
||
res.Failures = append(res.Failures, fmt.Sprintf("平台「%s」的登录账号已全部存在,已跳过", g.Platform))
|
||
continue
|
||
}
|
||
accounts := accountsToJSON(merged)
|
||
remark := exist.Remark
|
||
if g.Remark != "" {
|
||
remark = g.Remark
|
||
}
|
||
if _, uerr := models.Orm.QueryTable(new(models.BackendPasswordStore)).Filter("id", exist.ID).
|
||
Update(map[string]interface{}{"accounts": accounts, "remark": remark, "update_time": now}); uerr != nil {
|
||
res.Failed++
|
||
res.Failures = append(res.Failures, fmt.Sprintf("平台「%s」更新失败:%s", g.Platform, uerr.Error()))
|
||
continue
|
||
}
|
||
res.Updated++
|
||
continue
|
||
}
|
||
v := &models.BackendPasswordStore{
|
||
Tid: tid,
|
||
Platform: g.Platform,
|
||
URL: g.URL,
|
||
Accounts: accountsToJSON(g.Accounts),
|
||
Remark: g.Remark,
|
||
UserID: uid,
|
||
}
|
||
if _, ierr := models.Orm.Insert(v); ierr != nil {
|
||
res.Failed++
|
||
res.Failures = append(res.Failures, fmt.Sprintf("平台「%s」新增失败:%s", g.Platform, ierr.Error()))
|
||
continue
|
||
}
|
||
res.Created++
|
||
}
|
||
passwordReply(&c.Controller, 200, "导入完成", res)
|
||
}
|
||
|
||
// Export GET /backend/passwordStore/export 导出当前租户下本人全部记录
|
||
func (c *BackendPasswordStoreController) Export() {
|
||
cl, e := passwordClaims(&c.Controller, "backend")
|
||
if e != nil || cl.TenantId <= 0 {
|
||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
var list []models.BackendPasswordStore
|
||
if _, e = models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
||
Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID).
|
||
OrderBy("-id").All(&list); e != nil && e != orm.ErrNoRows {
|
||
passwordReply(&c.Controller, 500, "查询失败: "+e.Error(), nil)
|
||
return
|
||
}
|
||
res := make([]PasswordStoreItemDTO, 0, len(list))
|
||
for _, row := range list {
|
||
res = append(res, PasswordStoreItemDTO{
|
||
ID: row.ID,
|
||
Tid: row.Tid,
|
||
Platform: row.Platform,
|
||
URL: row.URL,
|
||
Accounts: accountsFromJSON(row.Accounts),
|
||
Remark: row.Remark,
|
||
UserID: row.UserID,
|
||
CreateTime: row.CreateTime,
|
||
UpdateTime: row.UpdateTime,
|
||
})
|
||
}
|
||
passwordReply(&c.Controller, 200, "success", map[string]interface{}{"list": res, "total": len(res)})
|
||
}
|