Files
yunzerwebsiteallinone/go/controllers/password_store.go
T
2026-09-30 00:57:36 +08:00

923 lines
30 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)})
}