sys-config.service.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. import { FastifyInstance } from 'fastify'
  2. import { Like, Not, Repository } from 'typeorm'
  3. import Decimal from 'decimal.js'
  4. import { ConfigType, SysConfig } from '../entities/sys-config.entity'
  5. import {
  6. CreateSysConfigBody,
  7. CreateTeamConfigBody,
  8. ListTeamConfigQuery,
  9. UpdateSysConfigBody,
  10. UpdateTeamConfigBody
  11. } from '../dto/sys-config.dto'
  12. import { User, UserRole } from '../entities/user.entity'
  13. import { Team } from '../entities/team.entity'
  14. import { TeamMembers } from '../entities/team-members.entity'
  15. import { Member } from '../entities/member.entity'
  16. export class SysConfigService {
  17. private app: FastifyInstance
  18. private sysConfigRepository: Repository<SysConfig>
  19. private userRepository: Repository<User>
  20. private teamRepository: Repository<Team>
  21. private teamMembersRepository: Repository<TeamMembers>
  22. private memberRepository: Repository<Member>
  23. constructor(app: FastifyInstance) {
  24. this.app = app
  25. this.sysConfigRepository = app.dataSource.getRepository(SysConfig)
  26. this.userRepository = app.dataSource.getRepository(User)
  27. this.teamRepository = app.dataSource.getRepository(Team)
  28. this.teamMembersRepository = app.dataSource.getRepository(TeamMembers)
  29. this.memberRepository = app.dataSource.getRepository(Member)
  30. }
  31. async getSysConfig(name: string, teamId?: number) {
  32. const where: any = { name }
  33. if (teamId !== undefined) {
  34. where.teamId = teamId
  35. }
  36. const sysConfig = await this.sysConfigRepository.findOneOrFail({ where })
  37. return sysConfig
  38. }
  39. async maxTransferAmount(defaultAmount: Decimal) {
  40. const sysConfig = await this.sysConfigRepository.findOne({ where: { name: 'max_transfer_amount' } })
  41. if (sysConfig) {
  42. return new Decimal(sysConfig.value)
  43. }
  44. await this.sysConfigRepository.save({ name: 'max_transfer_amount', value: defaultAmount.toString() })
  45. return defaultAmount
  46. }
  47. async replaceType(defaultValue: number) {
  48. try {
  49. const sysConfig = await this.sysConfigRepository.findOne({ where: { name: 'replace_type' } })
  50. if (sysConfig) {
  51. return Number(sysConfig.value)
  52. }
  53. await this.sysConfigRepository.save({ name: 'replace_type', value: defaultValue.toString() })
  54. } catch (e) {
  55. this.app.log.error(e, 'get replaceType error')
  56. }
  57. return defaultValue
  58. }
  59. async getSensitiveWords() {
  60. try {
  61. const config = await this.sysConfigRepository.findOne({ where: { name: 'sensitive_words' } })
  62. if (config) {
  63. return { value: config.value }
  64. }
  65. } catch (e) {
  66. this.app.log.error(e, 'get sensitiveWords error')
  67. }
  68. return null
  69. }
  70. async updateSensitiveWords(words: string) {
  71. try {
  72. const config = await this.sysConfigRepository.findOne({ where: { name: 'sensitive_words' } })
  73. if (config) {
  74. config.value = words
  75. const sysConfig = await this.sysConfigRepository.save(config)
  76. return {
  77. value: sysConfig.value
  78. }
  79. }
  80. await this.sysConfigRepository.save({ name: 'sensitive_words', value: words })
  81. } catch (e) {
  82. this.app.log.error(e, 'update sensitiveWords error')
  83. }
  84. }
  85. /**
  86. * 从 sysconfig 读取 domain_redirect 和 domain_land 配置
  87. * 将逗号分隔的字符串转换为列表返回
  88. * @param teamId 可选的团队ID,如果不提供则读取全局配置(teamId=0),如果为0则返回所有团队的配置(不筛选)
  89. * @returns 包含 domainRedirect 和 domainLand 两个列表的对象
  90. */
  91. async getDomainLists(teamId?: number): Promise<{ domainRedirect: string[]; domainLand: string[] }> {
  92. const targetTeamId = teamId !== undefined ? teamId : 0
  93. // 读取 domain_redirect 配置
  94. let domainRedirect: string[] = []
  95. try {
  96. if (targetTeamId === 0) {
  97. // teamId 为 0 时,查询所有团队的配置并合并
  98. const redirectConfigs = await this.sysConfigRepository.find({
  99. where: { name: 'domain_redirect' }
  100. })
  101. this.app.log.info(`查询 domain_redirect 配置,找到 ${redirectConfigs.length} 条记录`)
  102. const allDomains = new Set<string>()
  103. redirectConfigs.forEach(config => {
  104. this.app.log.info(`处理配置: teamId=${config.teamId}, name=${config.name}, value=${config.value}`)
  105. if (config && config.value) {
  106. const domains = config.value
  107. .split(',')
  108. .map(item => item.trim())
  109. .filter(item => item.length > 0)
  110. this.app.log.info(`解析出域名: ${domains.join(', ')}`)
  111. domains.forEach(domain => allDomains.add(domain))
  112. }
  113. })
  114. domainRedirect = Array.from(allDomains)
  115. this.app.log.info(`最终 domainRedirect 列表: ${domainRedirect.join(', ')}`)
  116. } else {
  117. // 查询指定团队的配置
  118. const redirectConfig = await this.sysConfigRepository.findOne({
  119. where: { name: 'domain_redirect', teamId: targetTeamId }
  120. })
  121. this.app.log.info(`查询指定团队配置: teamId=${targetTeamId}, 找到配置: ${redirectConfig ? '是' : '否'}`)
  122. if (redirectConfig && redirectConfig.value) {
  123. this.app.log.info(`配置值: ${redirectConfig.value}`)
  124. domainRedirect = redirectConfig.value
  125. .split(',')
  126. .map(item => item.trim())
  127. .filter(item => item.length > 0)
  128. }
  129. }
  130. } catch (e) {
  131. this.app.log.error(e, '读取 domain_redirect 配置失败')
  132. }
  133. // 读取 domain_land 配置
  134. let domainLand: string[] = []
  135. try {
  136. if (targetTeamId === 0) {
  137. // teamId 为 0 时,查询所有团队的配置并合并
  138. const landConfigs = await this.sysConfigRepository.find({
  139. where: { name: 'domain_land' }
  140. })
  141. this.app.log.info(`查询 domain_land 配置,找到 ${landConfigs.length} 条记录`)
  142. const allDomains = new Set<string>()
  143. landConfigs.forEach(config => {
  144. this.app.log.info(`处理配置: teamId=${config.teamId}, name=${config.name}, value=${config.value}`)
  145. if (config && config.value) {
  146. const domains = config.value
  147. .split(',')
  148. .map(item => item.trim())
  149. .filter(item => item.length > 0)
  150. this.app.log.info(`解析出域名: ${domains.join(', ')}`)
  151. domains.forEach(domain => allDomains.add(domain))
  152. }
  153. })
  154. domainLand = Array.from(allDomains)
  155. this.app.log.info(`最终 domainLand 列表: ${domainLand.join(', ')}`)
  156. } else {
  157. // 查询指定团队的配置
  158. const landConfig = await this.sysConfigRepository.findOne({
  159. where: { name: 'domain_land', teamId: targetTeamId }
  160. })
  161. this.app.log.info(`查询指定团队配置: teamId=${targetTeamId}, 找到配置: ${landConfig ? '是' : '否'}`)
  162. if (landConfig && landConfig.value) {
  163. this.app.log.info(`配置值: ${landConfig.value}`)
  164. domainLand = landConfig.value
  165. .split(',')
  166. .map(item => item.trim())
  167. .filter(item => item.length > 0)
  168. }
  169. }
  170. } catch (e) {
  171. this.app.log.error(e, '读取 domain_land 配置失败')
  172. }
  173. return {
  174. domainRedirect,
  175. domainLand
  176. }
  177. }
  178. async create(data: CreateSysConfigBody) {
  179. const where: any = { name: data.name }
  180. if (data.teamId !== undefined) {
  181. where.teamId = data.teamId
  182. }
  183. const existingConfig = await this.sysConfigRepository.findOne({ where })
  184. if (existingConfig) {
  185. throw new Error('操作失败')
  186. }
  187. const config = this.sysConfigRepository.create(data)
  188. return await this.sysConfigRepository.save(config)
  189. }
  190. async update(name: string, data: UpdateSysConfigBody, teamId?: number) {
  191. const config = await this.getSysConfig(name, teamId)
  192. Object.assign(config, data)
  193. return await this.sysConfigRepository.save(config)
  194. }
  195. async delete(name: string, teamId?: number) {
  196. const config = await this.getSysConfig(name, teamId)
  197. return await this.sysConfigRepository.remove(config)
  198. }
  199. async list(page: number = 0, size: number = 20, name?: string, type?: ConfigType, teamId?: number) {
  200. const queryBuilder = this.sysConfigRepository.createQueryBuilder('config')
  201. .where('config.name != :excludedName', { excludedName: 'sensitive_words' })
  202. if (name) {
  203. queryBuilder.andWhere('config.name LIKE :name', { name: `%${name}%` })
  204. }
  205. if (type) {
  206. queryBuilder.andWhere('config.type = :type', { type })
  207. }
  208. if (teamId !== undefined) {
  209. queryBuilder.andWhere('config.teamId = :teamId', { teamId })
  210. }
  211. // 排序:teamId为0的展示在最上方,其他的按照teamId排序
  212. queryBuilder
  213. .orderBy('CASE WHEN config.teamId = 0 THEN 0 ELSE 1 END', 'ASC')
  214. .addOrderBy('config.teamId', 'ASC')
  215. .addOrderBy('config.id', 'ASC')
  216. const total = await queryBuilder.getCount()
  217. const data = await queryBuilder
  218. .skip(page * size)
  219. .take(size)
  220. .getMany()
  221. return {
  222. data,
  223. meta: {
  224. page,
  225. size,
  226. total,
  227. totalPages: Math.ceil(total / size)
  228. }
  229. }
  230. }
  231. async getConfigTypes() {
  232. return Object.values(ConfigType)
  233. }
  234. // 获取用户的团队ID
  235. async getUserTeamId(userId: number, userRole: UserRole): Promise<number> {
  236. if (userRole === UserRole.ADMIN) {
  237. throw new Error('管理员不需要团队ID')
  238. }
  239. if (userRole === UserRole.USER) {
  240. throw new Error('普通用户无权限访问团队配置')
  241. }
  242. if (userRole === UserRole.TEAM) {
  243. // 队长从team表中获取teamId
  244. const team = await this.teamRepository.findOne({ where: { userId } })
  245. if (!team) {
  246. throw new Error('未找到该用户的团队信息')
  247. }
  248. return team.id
  249. } else {
  250. // 队员从team-members表中获取teamId
  251. const teamMember = await this.teamMembersRepository.findOne({ where: { userId } })
  252. if (!teamMember) {
  253. throw new Error('未找到该用户的团队成员信息')
  254. }
  255. return teamMember.teamId
  256. }
  257. }
  258. // 团队配置相关方法
  259. async createTeamConfig(data: CreateTeamConfigBody, userId: number, userRole: UserRole, adminTeamId?: number) {
  260. let teamId: number
  261. if (userRole === UserRole.ADMIN) {
  262. if (adminTeamId === undefined) {
  263. throw new Error('管理员操作团队配置时必须指定teamId')
  264. }
  265. teamId = adminTeamId
  266. } else {
  267. teamId = await this.getUserTeamId(userId, userRole)
  268. }
  269. const existingConfig = await this.sysConfigRepository.findOne({
  270. where: { name: data.name, teamId }
  271. })
  272. if (existingConfig) {
  273. throw new Error('该团队配置名称已存在')
  274. }
  275. const config = this.sysConfigRepository.create({ ...data, teamId })
  276. return await this.sysConfigRepository.save(config)
  277. }
  278. async updateTeamConfig(
  279. name: string,
  280. data: UpdateTeamConfigBody,
  281. userId: number,
  282. userRole: UserRole,
  283. adminTeamId?: number
  284. ) {
  285. let teamId: number
  286. if (userRole === UserRole.ADMIN) {
  287. if (adminTeamId === undefined) {
  288. throw new Error('管理员操作团队配置时必须指定teamId')
  289. }
  290. teamId = adminTeamId
  291. } else {
  292. teamId = await this.getUserTeamId(userId, userRole)
  293. }
  294. const config = await this.getSysConfig(name, teamId)
  295. Object.assign(config, data)
  296. return await this.sysConfigRepository.save(config)
  297. }
  298. async deleteTeamConfig(name: string, userId: number, userRole: UserRole, adminTeamId?: number) {
  299. let teamId: number
  300. if (userRole === UserRole.ADMIN) {
  301. if (adminTeamId === undefined) {
  302. throw new Error('管理员操作团队配置时必须指定teamId')
  303. }
  304. teamId = adminTeamId
  305. } else {
  306. teamId = await this.getUserTeamId(userId, userRole)
  307. }
  308. const config = await this.getSysConfig(name, teamId)
  309. return await this.sysConfigRepository.remove(config)
  310. }
  311. async getTeamConfig(name: string, userId: number, userRole: UserRole, adminTeamId?: number) {
  312. if (userRole === UserRole.ADMIN) {
  313. return await this.getSysConfig(name, adminTeamId)
  314. } else {
  315. const teamId = await this.getUserTeamId(userId, userRole)
  316. return await this.getSysConfig(name, teamId)
  317. }
  318. }
  319. async listTeamConfigs(query: ListTeamConfigQuery, userId: number, userRole: UserRole, adminTeamId?: number) {
  320. if (userRole === UserRole.ADMIN) {
  321. const { page = 0, size = 20, name, type } = query
  322. return await this.list(page, size, name, type, adminTeamId)
  323. } else {
  324. const teamId = await this.getUserTeamId(userId, userRole)
  325. const { page = 0, size = 20, name, type } = query
  326. return await this.list(page, size, name, type, teamId)
  327. }
  328. }
  329. /**
  330. * 从 parentId 递归查找 team 用户,直到找到 team 或到达顶层
  331. * @param parentId 父用户ID
  332. * @param maxDepth 最大递归深度,防止无限循环
  333. * @returns teamId,如果找不到则返回 0
  334. */
  335. private async findTeamIdByParentId(parentId: number, maxDepth: number = 10): Promise<number> {
  336. if (maxDepth <= 0 || !parentId || parentId <= 0) {
  337. return 0
  338. }
  339. try {
  340. const parentUser = await this.userRepository.findOne({ where: { id: parentId } })
  341. if (!parentUser) {
  342. return 0
  343. }
  344. // 如果父用户是 team 用户,直接查找对应的 team
  345. if (parentUser.role === UserRole.TEAM) {
  346. const team = await this.teamRepository.findOne({ where: { userId: parentId } })
  347. if (team) {
  348. return team.id
  349. }
  350. }
  351. // 如果父用户是推广员(promoter),可能是二级代理,继续向上查找
  352. if (parentUser.role === UserRole.PROMOTER && parentUser.parentId && parentUser.parentId > 0) {
  353. return await this.findTeamIdByParentId(parentUser.parentId, maxDepth - 1)
  354. }
  355. // 如果父用户有 parentId,继续向上查找
  356. if (parentUser.parentId && parentUser.parentId > 0) {
  357. return await this.findTeamIdByParentId(parentUser.parentId, maxDepth - 1)
  358. }
  359. } catch (error) {
  360. this.app.log.warn({ err: error, parentId }, 'Failed to find team by parentId')
  361. }
  362. return 0
  363. }
  364. async getUserTeamConfigs(userId?: number) {
  365. let teamId = 0
  366. if (userId) {
  367. // 方案1:优先从 member 表读取 teamId
  368. try {
  369. const member = await this.memberRepository.findOne({ where: { userId } })
  370. if (member && member.teamId && member.teamId > 0) {
  371. teamId = member.teamId
  372. }
  373. } catch (error) {
  374. this.app.log.warn({ err: error, userId }, 'Failed to get teamId from member table')
  375. }
  376. // 方案2:如果 member 表中没有有效的 teamId,从 parentId 递归查找 team
  377. if (teamId === 0) {
  378. try {
  379. const user = await this.userRepository.findOne({ where: { id: userId } })
  380. if (user && user.parentId && user.parentId > 0) {
  381. teamId = await this.findTeamIdByParentId(user.parentId)
  382. }
  383. } catch (error) {
  384. this.app.log.warn({ err: error, userId }, 'Failed to get teamId from parentId')
  385. }
  386. }
  387. }
  388. return await this.sysConfigRepository.find({
  389. where: { teamId },
  390. select: ['name', 'value'],
  391. order: { id: 'ASC' }
  392. })
  393. }
  394. // 创建团队默认配置
  395. async createDefaultTeamConfigs(teamId: number) {
  396. const defaultConfigs = [
  397. { name: 'hourly', value: '15', remark: '包时会员', type: ConfigType.Number },
  398. { name: 'daily', value: '28', remark: '包天会员', type: ConfigType.Number },
  399. { name: 'weekly', value: '58', remark: '包周会员', type: ConfigType.Number },
  400. { name: 'monthly', value: '88', remark: '包月会员', type: ConfigType.Number },
  401. { name: 'quarterly', value: '128', remark: '包季会员', type: ConfigType.Number },
  402. { name: 'yearly', value: '198', remark: '包年会员', type: ConfigType.Number },
  403. { name: 'lifetime', value: '299', remark: '终生会员', type: ConfigType.Number },
  404. { name: 'single', value: '6', remark: '单片价格', type: ConfigType.Number },
  405. { name: 'preview_duration', value: '30', remark: '试看时间(秒)', type: ConfigType.Number },
  406. { name: 'invite_reward_count', value: '3', remark: '邀请奖励所需次数', type: ConfigType.Number },
  407. { name: 'invite_ip_limit', value: '1', remark: '同一IP每日邀请注册限制次数', type: ConfigType.Number }
  408. ]
  409. const configs = defaultConfigs.map(config =>
  410. this.sysConfigRepository.create({
  411. ...config,
  412. teamId
  413. })
  414. )
  415. return await this.sysConfigRepository.save(configs)
  416. }
  417. }